3

If you call the ShowAsync command on a MessageDialog object when another MessageDialog object has already been displayed to the user but not dismissed (i.e. you show a popup when another one is already up), an UnauthorizedAccessException is thrown. This can make things difficult when you have multiple threads attempting to alert the user at the same time.

My current (stopgap) solution is merely to surround the ShowAsync call with a try/catch block and swallow the exception. This undesirably leads to the user missing out on subsequent notifications. The only other way around this that I can think of is to manually implement some sort of popup queue. This seems like an inordinate amount of work, however, considering other frameworks (like Windows Phone) do not have this issue and will merely display the popups one after another as the user dismisses them.

Is there another way to solve this problem?

jokeefe
  • 1,836
  • 4
  • 22
  • 27
  • check this out: http://stackoverflow.com/questions/12722490/messagedialog-showasync-throws-accessdenied-exception-on-second-dialog – MUG4N Jan 23 '13 at 20:20
  • Thanks, @MUG4N, but that sounds like it closes the initial dialog, which still causes the user to miss out on a popup, so that doesn't solve my problem. – jokeefe Jan 23 '13 at 20:47

2 Answers2

11

You can easy do it with this extension method:

public static class MessageDialogShower
{
    private static SemaphoreSlim _semaphore;

    static MessageDialogShower()
    {
        _semaphore = new SemaphoreSlim(1);
    }

    public static async Task<IUICommand> ShowDialogSafely(this MessageDialog dialog)
    {
        await _semaphore.WaitAsync();
        var result = await dialog.ShowAsync();
        _semaphore.Release();
        return result;
    }
}
Filip Skakun
  • 31,624
  • 6
  • 74
  • 100
Dave Smits
  • 1,871
  • 14
  • 22
  • This is by far the simplest solution to this problem. Thank you very much! – jokeefe Jan 23 '13 at 22:44
  • 1
    This is probably how Microsoft should have implemented the underlying functionality of showing a `MessageDialog` in the first place! I'm glad I now know about `SemaphoreSlim`. – Ne0 Aug 07 '14 at 10:02
10

There are many ways to approach it and the choice might depend on your skills, requirements and preferences.

My personal choice is to avoid using dialog boxes altogether since they are bad for user experience (evil). There are then alternative solutions like displaying a separate screen/page with the UI requiring user to provide some input when it really is required or displaying a non-modal popup somewhere on the side/edge/corner if the user input is optional and hiding it after a moment or some other sort of notification that doesn't break user flow.

If you disagree or don't have the time, resources or skills to implement an alternative - you can create some sort of a wrapper around MessageDialog.ShowAsync() call to either queue or ignore new requests while a dialog is already shown.

This class has extension methods to allow to either ignore a new show request when another dialog is already displayed or queue up the requests:

/// <summary>
/// MessageDialog extension methods
/// </summary>
public static class MessageDialogExtensions
{
    private static TaskCompletionSource<MessageDialog> _currentDialogShowRequest;

    /// <summary>
    /// Begins an asynchronous operation showing a dialog.
    /// If another dialog is already shown using
    /// ShowAsyncQueue or ShowAsyncIfPossible method - it will wait
    /// for that previous dialog to be dismissed before showing the new one.
    /// </summary>
    /// <param name="dialog">The dialog.</param>
    /// <returns></returns>
    /// <exception cref="System.InvalidOperationException">This method can only be invoked from UI thread.</exception>
    public static async Task ShowAsyncQueue(this MessageDialog dialog)
    {
        if (!Window.Current.Dispatcher.HasThreadAccess)
        {
            throw new InvalidOperationException("This method can only be invoked from UI thread.");
        }

        while (_currentDialogShowRequest != null)
        {
            await _currentDialogShowRequest.Task;
        }

        var request = _currentDialogShowRequest = new TaskCompletionSource<MessageDialog>();
        await dialog.ShowAsync();
        _currentDialogShowRequest = null;
        request.SetResult(dialog);
    }

    /// <summary>
    /// Begins an asynchronous operation showing a dialog.
    /// If another dialog is already shown using
    /// ShowAsyncQueue or ShowAsyncIfPossible method - it will wait
    /// return immediately and the new dialog won't be displayed.
    /// </summary>
    /// <param name="dialog">The dialog.</param>
    /// <returns></returns>
    /// <exception cref="System.InvalidOperationException">This method can only be invoked from UI thread.</exception>
    public static async Task ShowAsyncIfPossible(this MessageDialog dialog)
    {
        if (!Window.Current.Dispatcher.HasThreadAccess)
        {
            throw new InvalidOperationException("This method can only be invoked from UI thread.");
        }

        while (_currentDialogShowRequest != null)
        {
            return;
        }

        var request = _currentDialogShowRequest = new TaskCompletionSource<MessageDialog>();
        await dialog.ShowAsync();
        _currentDialogShowRequest = null;
        request.SetResult(dialog);
    }
}

Test

// This should obviously be displayed
var dialog = new MessageDialog("await ShowAsync", "Dialog 1");
await dialog.ShowAsync();

// This should be displayed because we awaited the previous request to return
dialog = new MessageDialog("await ShowAsync", "Dialog 2");
await dialog.ShowAsync(); 

// All other requests below are invoked without awaiting
// the preceding ones to complete (dialogs being closed)

// This will show because there is no dialog shown at this time
dialog = new MessageDialog("ShowAsyncIfPossible", "Dialog 3");
dialog.ShowAsyncIfPossible();

// This will not show because there is a dialog shown at this time
dialog = new MessageDialog("ShowAsyncIfPossible", "Dialog 4");
dialog.ShowAsyncIfPossible();

// This will show after Dialog 3 is dismissed
dialog = new MessageDialog("ShowAsyncQueue", "Dialog 5");
dialog.ShowAsyncQueue();

// This will not show because there is a dialog shown at this time (Dialog 3)
dialog = new MessageDialog("ShowAsyncIfPossible", "Dialog 6");
dialog.ShowAsyncIfPossible();

// This will show after Dialog 5 is dismissed
dialog = new MessageDialog("ShowAsyncQueue", "Dialog 7");
dialog.ShowAsyncQueue();

// This will show after Dialog 7 is dismissed
dialog = new MessageDialog("ShowAsyncQueue", "Dialog 8");
dialog.ShowAsyncQueue();
Community
  • 1
  • 1
Filip Skakun
  • 31,624
  • 6
  • 74
  • 100
  • Thanks for the suggestion, but in my case, the modal dialogs are necessary. The "ignore new request" method is what I already have in place via try/catch block. The "queue" method was my other suggestion that I was hoping to avoid. – jokeefe Jan 23 '13 at 21:30
  • I added some code to show how to queue up the requests. It does require that you always use `ShowAsyncQueue` or `ShowAsyncIfPossible` instead of `ShowAsync()` since otherwise I would need to catch exceptions and keep retrying which would be a pretty bad design. – Filip Skakun Jan 23 '13 at 21:46
  • Thanks for the code, @Filip Skakun. I ended up accepting your answer over the other one, because yours actually seems to have slightly better performance. – jokeefe Jan 23 '13 at 22:49
  • What do you mean by better performance? Does the dialog show up quicker or? – Filip Skakun Jan 23 '13 at 23:01
  • Yes, after the first dialog is dismissed, the second dialog appears to show up a fraction of a second faster. – jokeefe Jan 24 '13 at 15:05