0
votes

We are working to migrate our INotification/IConfirmation dialogs to Prism's DialogService. One trouble we're having is supporting a customization we made to PopupWindowAction to optionally "bring to front" an existing non-modal dialog if the action was invoked a second time.

Can this be accomplished with DialogService?

Specifically, if a non-modal window is minimized or inactive (behind another window), how can we activate it? We are currently using code like the following which recognizes that a specific INotification was previously displayed and simply activates it.

if (BringToFrontIfExisting)
{
    if (NotificationWindowMap.TryGetValue(notification, out Window window))
    {
        if (window.WindowState == WindowState.Minimized)
        {
            window.WindowState = WindowState.Normal;
        }

        window.Activate();

        return;
    }
}

We looked into extending DialogService; unfortunately it seems we'd need to extend ShowDialogInternal which is not virtual. Nor does IDialogWindow expose WindowState or Activate.

It seems we might be able to accomplish this externally by registering an instance of the dialog and managing this activation external to the DialogService. Though I'd like to work with Prism as much as possible here and minimize the complexity for the ViewModel.

2

2 Answers

1
votes

Can this be accomplished with DialogService?

No, this is not supported by Prism's DialogService.

[...] unfortunately it seems we'd need to extend ShowDialogInternal which is not virtual.

As you have already noticed, there are no virtual methods, so you cannot derive from it and override its behavior. You have to create your own implementation, but at least you can copy the default implementation from GitHub and adapt it to your needs.

Nor does IDialogWindow expose WindowState or Activate.

The IDialogWindow interface has a limitation or a design choice in that it does not provide the same methods or properties as a Window has. If you can guarantee that your dialog host is a type derived from Window, you can just cast the IDialogWindow instance in your custom dialog service to Window.

A more generalized variant would be to create a custom dialog window interface:

  • Create your own ICustomDialogWindow interface that inherits IDialogWindow for compatibility
  • Add the missing properties and methods to it, like Activate
  • Create a CustomWindow by e.g. deriving from Window and implementing ICustomDialogWindow
  • Register this window type as the new default dialog host window in the container
    containerRegistry.RegisterDialogWindow<ConfirmationWindow>();
    

Now you can cast the dialog window instance to the ICustomDialogWindow interface in your custom dialog service and Activate it. Alternatively, you could also implement the activation behavior in the dialog service itself, then the interface just serves to expose the needed methods and properties of the window to do so. Implementing the activation in the concrete dialog window class is more flexible as it can be specialized to the underlaying type or applied when a common implementation is not possible.

If you already use Prism 8 and allow for multiple dialog hosts, you can implement the ICustomDialogWindow interface in all window hosts and throw an exception if a host does not or just provide different behaviors depending on the type, e.g. an IDialogWindow would just not support activation. In general, you have to create the dialog host window types yourself to implement the interfaces, so this should not be a problem.

Specifically, if a non-modal window is minimized or inactive (behind another window), how can we activate it?

The code that you provided should work fine in this scenario. You just have to decide whether you want to implement it in the dialog service or your custom window host.

It seems we might be able to accomplish this externally by registering an instance of the dialog and managing this activation external to the DialogService.

Then you would completely work around the dialog service, as it does not expose instances of the created dialogs. You would have to find them from the application windows collection and handle activation yourself, which would be almost half a dialog service, so you could have created a custom service in the first place.

1
votes

Below is the solution I came up with to solve my original question. In essence, DialogServiceEx supports optionally activating an existing modeless dialog rather than displaying a second copy. Additionally, the dialog owner is also optional; this allows the dialog to be independent from the owner (can be independently minimized, etc.)

Note: The service is not thread-safe. The assumption is that dialogs will only be shown on the UI thread.

public class DialogServiceEx : DialogService, IDialogServiceEx
{
    private readonly Dictionary<string, IDialogWindow> _reusableDialogWindows = new Dictionary<string, IDialogWindow>();

    public DialogServiceEx(IContainerExtension containerExtension)
        : base(containerExtension)
    {
    }

    public void Show(
        string name,
        IDialogParameters parameters,
        Action<IDialogResult> callback,
        bool reuseExistingWindow,
        bool setOwner = true)
            => ShowDialogInternalEx(name, parameters, callback, null, reuseExistingWindow, setOwner);

    public void Show(
        string name,
        IDialogParameters parameters,
        Action<IDialogResult> callback,
        string windowName,
        bool reuseExistingWindow,
        bool setOwner = true)
            => ShowDialogInternalEx(name, parameters, callback, windowName, reuseExistingWindow, setOwner);

    private void ShowDialogInternalEx(
        string name,
        IDialogParameters parameters,
        Action<IDialogResult> callback,
        string windowName,
        bool reuseExistingWindow,
        bool setOwner)
    {
        string dialogKey = $"{name}-{windowName}";
        if (reuseExistingWindow &&
            _reusableDialogWindows.TryGetValue($"{name}-{windowName}", out IDialogWindow dialogWindow))
        {
            dialogWindow.Show();
            if (dialogWindow is Window window)
            {
                // NOTE: IDialogWindow should always be a Window under WPF.

                if (window.WindowState == WindowState.Minimized)
                {
                    window.WindowState = WindowState.Normal;
                }

                window.Activate();
                return;
            }
        }

        dialogWindow = CreateDialogWindow(windowName);

        if (reuseExistingWindow)
        {
            _reusableDialogWindows.Add(dialogKey, dialogWindow);
            dialogWindow.Closed +=
                (_, __) =>
                {
                    Debug.Assert(_reusableDialogWindows.ContainsKey(dialogKey), "Expect single-threaded access only.");
                    _reusableDialogWindows.Remove(dialogKey);
                };
        }

        ConfigureDialogWindowEvents(dialogWindow, callback);
        ConfigureDialogWindowContent(name, dialogWindow, parameters ?? new DialogParameters());

        if (setOwner == false)
        {
            dialogWindow.Owner = null;
        }

        ShowDialogWindow(dialogWindow, false);
    }
}