Jeremy Likness' Blog

Simple Dialog Service in Silverlight

I noticed on the forums there are a lot of users not comfortable with asynchronous programming who struggle a bit in Silverlight with getting their arms around the concept of a dialog box. In other environments, you can simply shoot out a dialog, wait for the response, and continue. In Silverlight, of course, the action is asynchronous. I would argue it should be this way most of the time.

The problem is that many people tend to take the approach of trying to force the process into a synchronous one, instead of changing the way they think and approach the application. There is a reason why processes are asynchronous in Silverlight. There is one main UI thread, and a blocking process would block the entire thread and effectively freeze the Silverlight application. Having the process asynchronous allows you to continue to render graphics elements, perform actions in the background, even display a soothing animation while you await the user's response.

I spoke to a more highly decoupled approach in a post awhile ago that was more an experiment with the event aggregator: Decoupled Child Window Dialogs with Silverlight and PRISM. Here, I want to show the more straightforward approach.

The first step is to choose what the dialog will be displayed with. In Silverlight, the ChildWindow makes perfect sense because it is a modal dialog that appears, waits for the user response, and then saves the response. We'll create a new child window and call it Dialog.xaml. It looks like this:

<controls:ChildWindow x:Class="SimpleDialog.Dialog"
           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"
           Width="400" Height="300" 
           Title="{Binding Title}">
    <Grid x:Name="LayoutRoot" Margin="2">
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <TextBlock TextWrapping="Wrap" Grid.Row="0" Text="{Binding Message}"/>
        <Button x:Name="CancelButton" Content="Cancel" Click="CancelButton_Click" Width="75" Height="23" HorizontalAlignment="Right" Margin="0,12,0,0" Grid.Row="1" />
        <Button x:Name="OKButton" Content="OK" Click="OKButton_Click" Width="75" Height="23" HorizontalAlignment="Right" Margin="0,12,79,0" Grid.Row="1" />
    </Grid>
</controls:ChildWindow>

You'll notice I made very few changes from the provided template. The key here is that I changed the title to bind with the Title property, and added a text block to display a message from the Message property.

Because this is a simple dialog box, I really feel a view model is overkill. Some purists will insist on this but I argue the functionality is simple enough that we don't need the extra overhead. We'll be using an interface to the solution down the road that we can easily unit test, and this makes the dialog function (which lives entirely in the UI) a self-contained implementation.

Next, I made a few changes to the code behind:

public partial class Dialog : ChildWindow
{
    public Action<bool> CloseAction { get; set; }

    public static readonly DependencyProperty MessageProperty = DependencyProperty.Register(
        "Message",
        typeof(string),
        typeof(Dialog),
        null);


    public string Message
    {
        get { return GetValue(MessageProperty).ToString(); }
        set { SetValue(MessageProperty, value); }
    }

    public Dialog()
    {
        InitializeComponent();
        DataContext = this;
    }        

    public Dialog(bool allowCancel)
        : this()
    {
        CancelButton.Visibility = allowCancel ? Visibility.Visible : Visibility.Collapsed;
    }

    private void OKButton_Click(object sender, RoutedEventArgs e)
    {
        this.DialogResult = true;            
    }

    private void CancelButton_Click(object sender, RoutedEventArgs e)
    {
        this.DialogResult = false;           
    }
}

Again, not too many changes here. I added the Message property as a dependency property, and in the constructor I set the data context to itself. This allows me to bind to the title and message. This allows us to simply set the message on the dialog and have it appear. I also added an action for it to retain when closed. This will store a callback so that it can notify the host of the user's final actions. Finally, there is a method that conditions whether or not there is the option for a cancel button. For alerts, we'll simply show an OK button. For confirmations, we'll show the Cancel button as well.

Next is a simple interface to the dialog. Nothing in our application should know or care how the dialog is displayed. There is simply a mechanism to display the dialog and possibly acquire a response. The interface looks like this:

public interface IDialogService
{
    void ShowDialog(string title, string message, bool allowCancel, Action<bool> response);
}

In our simple demonstration, I'm allowing for a title, a message, whether or not you want to show the cancel button for confirmations, and then an action to call with the response. With this simple interface we have all we need to wire in unit tests for services and controls that rely on the dialog. You can create your own mock object that implements the IDialogService interface and returns whatever response you want to stage for the unit test.

In the production application, we'll need to show a real dialog. Here is the class that handles it:

public class DialogService : IDialogService
{
    #region IDialogService Members

    public void ShowDialog(string title, string message, bool allowCancel, Action<bool> response)
    {
        var dialog = new Dialog(allowCancel) {Title = title, Message = message, CloseAction = response};
        dialog.Closed += DialogClosed;
        dialog.Show();
    }

    static void DialogClosed(object sender, EventArgs e)
    {
        var dialog = sender as Dialog;
        if (dialog != null)
        {
            dialog.Closed -= DialogClosed; 
            dialog.CloseAction(dialog.DialogResult == true);
        }
    }

    #endregion
}

This is fairly straightforward. When called, we'll create an instance of the dialog and set the various properties, including the callback. We wire into the closed event. Whether the user responds by clicking a button or closing the dialog, this event is fired.

There is a reason why we are using the snippet DialogResult == true. The result is null-able, so we cannot simply refer to the value itself and make a true/false decision (null, by definition, is unknown). So only if the user explicitly provides a true response by clicking the OK button will the expression evaluate to true. Otherwise, we assume false and call back with the response.

There is also a very important step here that is important to recognize. Many beginners fail to catch this subtle step and end up with memory leaks. When the closed event fires, the dialog box effectively closes. Typically, there would be no more references to it (it is no longer in the visual tree) and it can be cleaned up by garbage collection. However, there is a fatal mistake you can make that will result in the dialog box never going away.

That mistake has to do with how events work. When we register the closed event, the thought process is often that the dialog box has an event, and therefore knows to call back to the dialog service. This is flawed, however. A reference exists from the dialog service to the dialog box. The dialog service effectively "listens" for the event, and when it is raised, will respond. We must unregister the event like this:

dialog.Closed -= DialogClosed;

Otherwise, the reference remains and garbage collection will never claim the dialog box resource. In a long running application, this could prove to be a serious flaw. This line is not included in the code sample download. I encourage you to run with windbg or even step through debug in Visual Studio and watch the object graphs and reference counts as you fire multiple dialogs. Then, add the line of code above and re-run it to see the difference when you appropriately unregister the event.

To demonstrate the use of the service, I put together a quick little page with a few buttons and a text block:

<UserControl x:Class="SimpleDialog.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
    mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480">
  <Grid x:Name="LayoutRoot">
        <StackPanel Orientation="Vertical">
            <Button x:Name="ToggleEnabled" 
                    HorizontalAlignment="Center"
                    Margin="5" Width="Auto" Content="Disable Dialog Button" Click="ToggleEnabled_Click"/>
            <Button x:Name="DialogButton" 
                    HorizontalAlignment="Center"
                    Margin="5" Width="Auto" Content="DialogButton" Click="DialogButton_Click"/>
            <TextBlock x:Name="TextDialog" 
                       HorizontalAlignment="Center"
                       Margin="5" Width="Auto" Text="Result Will Show Here"/>
        </StackPanel>
    </Grid>
</UserControl>

The first button is a toggle to enable or disable the second button. It shows how to receive a response and act based on the user input. If the user confirms, the button is toggled to enabled or disabled. If the user cancels or closes the dialog, the state of the button remains the same. The second button raises a dialog, and simply shows the result. Because it is an alert dialog, we know a false response indicates the user closed the window instead of clicking OK.

Here is the code behind:

public partial class MainPage : UserControl
{
    private bool _toggleState = true;
    private readonly IDialogService _dialog = new DialogService();

    public MainPage()
    {
        InitializeComponent();
    }

    private void ToggleEnabled_Click(object sender, RoutedEventArgs e)
    {
        _dialog.ShowDialog(
            _toggleState ? "Disable Button" : "Enable Button",
            _toggleState
                ? "Are you sure you want to disable the dialog button?"
                : "Are you sure you want to enable the dialog button?",
            true,
            r =>
                {
                    if (r)
                    {
                        _toggleState = !_toggleState;
                        DialogButton.IsEnabled = _toggleState;
                        ToggleEnabled.Content = _toggleState ? "Disable Dialog Button" : "Enable Dialog Button";
                    }
                });
    }

    private void DialogButton_Click(object sender, RoutedEventArgs e)
    {
        _dialog.ShowDialog(
            "Confirm This",
            "You have to either close the window or click OK to confirm this.",
            false,
            r => { TextDialog.Text = r ? "You clicked OK." : "You closed the dialog."; });
    }
}

Notice that in the click events, we call to the dialog service and pass it anonymous methods for the return types. We could point to real methods on the class as well. The important part is to change your thought process from synchronous step one => wait => step two to asynchronous step one => delegate to step two, then step two is called when ready.

In a production application, instead of creating an instance of the dialog box, we would either register it with a container:

Container.RegisterInstance<IDialogService>(Container.Resolve<DialogService>());

And inject it in a constructor, or use something like MEF and import the dialog service:

[Import]
public IDialogService Dialog { get; set; }

We would simply flag the implementation as the export or use the InheritedExport attribute on the interface itself.

Click here to download the source code for this example.

Jeremy Likness

On Jan 22 2010 11:34 PMBy jlikness Silverlight, AsyncWith 5 Comments

Comments (5)

  1. Jeremy,

    Nice article, but are you sure about the part with the potential memory leak?

    Quote:
    This is flawed, however. A reference exists from the dialog service to the dialog box. The dialog service effectively "listens" for the event.

    Isn't the delegate reference from the dialog to the even listener in your dialog service technically the same as the reference from your UI to the callback action - just a pointer towards a method?

    Cheers,
    Philipp

  2. Correct. There are nuances however, most specifically that the event can handle multiple registrations and each one must be de-registered ("unhooked") for it to release, as opposed to the single delegate that is provided via the action mechanism. There are probably better examples of the event-causes-memory-leak issue in Silverlight than this.

  3. Thanks - btw, that link above takes me to a page with no content and a Windows 7 phone flash advertisement ?

  4. I am using SL4 and added the line you suggested like this:
    static void DialogClosed(object sender, EventArgs e)
    {
    // next 1 line added by lvo per article by Jeremy likness
    ((Dialog)sender).Closed -= DialogClosed;
    ((Dialog)sender).CloseAction(((Dialog)sender).DialogResult == true);
    }
    monitored with TaskManager - memory use continues to increase with each click/display of childwindow. I waited serveral minutes fo r garbage collect - no changes.

Leave a Comment