23
votes

My application has several independent "top-level" windows, which all have completely different functions/workflows.

I am currently using ShowDialog() to make a WPF Window modal. The modal window is a child of one of the main windows. However, it is blocking all the top-level windows once it is open. I would like the dialog to block ONLY the parent window it was launched from. Is this possible?

I'm not sure if it matters, but the window that opens the dialog is the initial window of the app--so all other top-level windows are opened from it.

2

2 Answers

10
votes

One option is to start the windows that you don't want affected by the dialog on a different thread. This may result in other issues for your application, but if those windows do really encapsulate different workflows, that may not be an issue. Here is some sample code I wrote to verify that this works:

<Window x:Class="ModalSample.MyWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="{Binding Identifier}" Height="150" Width="150">
    <StackPanel>
        <TextBox Text="{Binding Identifier}" />
        <Button Content="Open Normal Child" Click="OpenNormal_Click" />
        <Button Content="Open Independent Child" Click="OpenIndependent_Click" />
        <Button Content="Open Modal Child" Click="OpenModal_Click" />
    </StackPanel>
</Window>

using System.ComponentModel;
using System.Threading;
using System.Windows;

namespace ModalSample
{
    /// <summary>
    /// Interaction logic for MyWindow.xaml
    /// </summary>
    public partial class MyWindow : INotifyPropertyChanged
    {
        public MyWindow()
        {
            InitializeComponent();
            DataContext = this;
        }

        private int child = 1;

        private string mIdentifier = "Root";
        public string Identifier
        {
            get { return mIdentifier; }
            set
            {
                if (mIdentifier == value) return;
                mIdentifier = value;
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("Identifier"));
            }
        }

        private void OpenNormal_Click(object sender, RoutedEventArgs e)
        {
            var window = new MyWindow {Identifier = Identifier + "-N" + child++};
            window.Show();
        }

        private void OpenIndependent_Click(object sender, RoutedEventArgs e)
        {
            var thread = new Thread(() =>
                {
                    var window = new MyWindow {Identifier = Identifier + "-I" + child++};
                    window.Show();

                    window.Closed += (sender2, e2) => window.Dispatcher.InvokeShutdown();

                    System.Windows.Threading.Dispatcher.Run();
                });

            thread.SetApartmentState(ApartmentState.STA);
            thread.Start();
        }

        private void OpenModal_Click(object sender, RoutedEventArgs e)
        {
            var window = new MyWindow { Identifier = Identifier + "-M" + child++ };
            window.ShowDialog();
        }

        public event PropertyChangedEventHandler PropertyChanged;
    }
}

I sourced this blog post for running a WPF window on a different thread.

12
votes

I had the same problem and implemented the modal dialog behavior as described in this post: http://social.msdn.microsoft.com/Forums/vstudio/en-US/820bf10f-3eaf-43a8-b5ef-b83b2394342c/windowsshowmodal-to-parentowner-window-only-not-entire-application?forum=wpf

I also tried a multiple UI thread approach, but this caused problems with third-party libraries (caliburn micro & telerik wpf controls), since they are not built to be used in multiple UI threads. It is possible to make them work with multiple UI threads, but I prefer a simpler solution...

If you implement the dialog as described, you can not use the DialogResult property anymore, since it would cause a "DialogResult can be set only after Window is created and shown as dialog" exception. Just implement your own property and use it instead.

You need the following windows API reference:

/// <summary>
/// Enables or disables mouse and keyboard input to the specified window or control. 
/// When input is disabled, the window does not receive input such as mouse clicks and key presses. 
/// When input is enabled, the window receives all input.
/// </summary>
/// <param name="hWnd"></param>
/// <param name="bEnable"></param>
/// <returns></returns>
[DllImport("user32.dll")]
private static extern bool EnableWindow(IntPtr hWnd, bool bEnable);

Then use this:

// get parent window handle
IntPtr parentHandle = (new WindowInteropHelper(window.Owner)).Handle;
// disable parent window
EnableWindow(parentHandle, false);
// when the dialog is closing we want to re-enable the parent
window.Closing += SpecialDialogWindow_Closing;
// wait for the dialog window to be closed
new ShowAndWaitHelper(window).ShowAndWait();
window.Owner.Activate();

This is the event handler which re-enables the parent window, when the dialog is closed:

private void SpecialDialogWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
    var win = (Window)sender;
    win.Closing -= SpecialDialogWindow_Closing;
    IntPtr winHandle = (new WindowInteropHelper(win)).Handle;
    EnableWindow(winHandle, false);

    if (win.Owner != null)
    {
        IntPtr parentHandle = (new WindowInteropHelper(win.Owner)).Handle;
        // reenable parent window
        EnableWindow(parentHandle, true);
    }
}

And this is the ShowAndWaitHelper needed to achieve the modal dialog behavior (this blocks the execution of the thread, but still executes the message loop.

private sealed class ShowAndWaitHelper
{
    private readonly Window _window;
    private DispatcherFrame _dispatcherFrame;
    internal ShowAndWaitHelper(Window window)
    {
        if (window == null)
        {
            throw new ArgumentNullException("window");
        }
        _window = window;
    }
    internal void ShowAndWait()
    {
        if (_dispatcherFrame != null)
        {
            throw new InvalidOperationException("Cannot call ShowAndWait while waiting for a previous call to ShowAndWait to return.");
        }
        _window.Closed += OnWindowClosed;
        _window.Show();
        _dispatcherFrame = new DispatcherFrame();
        Dispatcher.PushFrame(_dispatcherFrame);
    }
    private void OnWindowClosed(object source, EventArgs eventArgs)
    {
        if (_dispatcherFrame == null)
        {
            return;
        }
        _window.Closed -= OnWindowClosed;
        _dispatcherFrame.Continue = false;
        _dispatcherFrame = null;
    }
}