Search code examples
c#multithreadingwinformswindowshowdialog

C# WinForms: Form.ShowDialog() with IWin32Window owner parameter in a different thread


I am creating a C# VSTO addin and am having trouble with setting the owner window parameter in Form.ShowDialog() when the form is shown in a secondary thread and the owner window is on the main thread.

When using VSTO, Excel only supports changes to the Excel object model on the main thread (it can be done on a separate thread but is dangerous and will throw COM exceptions if Excel is busy). I would like to show a progress form while executing a long operation. To make the progress form fluid, I show the form on a separate thread and update the progress asynchronously from the main thread using Control.BeginInvoke(). This all works fine, but I seem to only be able to show the form using Form.ShowDialog() with no parameters. If I pass an IWin32Window or NativeWindow as a parameter to ShowDialog, the form freezes up and does not update the progress. This may be because the owner IWin32Window parameter is a Window that exists on the main thread and not the secondary thread that the progress form is displayed on.

Is there any trick I can try to pass a IWin32Window to the ShowDialog function when the form is on a separate thread. Technically I don't need to set the form's owner, but rather the form's parent if there is such a difference.

I'd like my dialog to be linked with the Excel Window so that when Excel is minimized or maximized, the dialog will be hidden or shown accordingly.

Please note that I have already tried going the BackgroundWorker route and it was not successful for what I was trying to accomplish.

----Updated with sample code:

Below is a trimmed down version of what I am trying to do and how I am trying to do it. The MainForm is not actually used in my application, as I am trying to use it to represent the Excel Window in a VSTO application.

Program.cs:

using System;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new MainForm());
        }
    }
}

MainForm.cs:

using System;
using System.Windows.Forms;
using System.Threading;

namespace WindowsFormsApplication1
{
    public partial class MainForm : Form
    {
        public ManualResetEvent SignalEvent = new ManualResetEvent(false);
        private ProgressForm _progressForm;
        public volatile bool CancelTask;

        public MainForm()
        {
            InitializeComponent();
            this.Name = "MainForm";
            var button = new Button();
            button.Text = "Run";
            button.Click += Button_Click;
            button.Dock = DockStyle.Fill;
            this.Controls.Add(button);
        }

        private void Button_Click(object sender, EventArgs e)
        {
            CancelTask = false;
            ShowProgressFormInNewThread();
        }

        internal void ShowProgressFormInNewThread()
        {
            var thread = new Thread(new ThreadStart(ShowProgressForm));
            thread.Start();

            //The main thread will block here until the signal event is set in the ProgressForm_Load.
            //this will allow us to do the work load in the main thread (required by VSTO projects that access the Excel object model),
            SignalEvent.WaitOne();
            SignalEvent.Reset();

            ExecuteTask();
        }

        private void ExecuteTask()
        {
            for (int i = 1; i <= 100 && !CancelTask; i++)
            {
                ReportProgress(i);
                Thread.Sleep(100);
            }
        }

        private void ReportProgress(int percent)
        {
            if (CancelTask)
                return;
            _progressForm.BeginInvoke(new Action(() => _progressForm.UpdateProgress(percent)));
        }

        private void ShowProgressForm()
        {
            _progressForm = new ProgressForm(this);
            _progressForm.StartPosition = FormStartPosition.CenterParent;

            //this works, but I want to pass an owner parameter
            _progressForm.ShowDialog();

            /*
             * This gives an exception:
             * An unhandled exception of type 'System.InvalidOperationException' occurred in System.Windows.Forms.dll
             * Additional information: Cross-thread operation not valid: Control 'MainForm' accessed from a thread other than the thread it was created on.
             */
            //var window = new Win32Window(this);
            //_progressForm.ShowDialog(window);

        }

    }
}

ProgressForm.cs:

using System;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    public partial class ProgressForm : Form
    {
        private ProgressBar _progressBar;
        private Label _progressLabel;
        private MainForm _mainForm;

        public ProgressForm(MainForm mainForm)
        {
            InitializeComponent();
            _mainForm = mainForm;
            this.Width = 300;
            this.Height = 150;
            _progressBar = new ProgressBar();
            _progressBar.Dock = DockStyle.Top;
            _progressLabel = new Label();
            _progressLabel.Dock = DockStyle.Bottom;
            this.Controls.Add(_progressBar);
            this.Controls.Add(_progressLabel);
            this.Load += ProgressForm_Load;
            this.Closed += ProgressForm_Close;
        }

        public void UpdateProgress(int percent)
        {
            if(percent >= 100)
                Close();

            _progressBar.Value = percent;
            _progressLabel.Text = percent + "%";
        }

        public void ProgressForm_Load(object sender, EventArgs e)
        {
            _mainForm.SignalEvent.Set();
        }

        public void ProgressForm_Close(object sender, EventArgs e)
        {
            _mainForm.CancelTask = true;
        }

    }
}

Win32Window.cs:

using System;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    public class Win32Window : IWin32Window
    {
        private readonly IntPtr _handle;

        public Win32Window(IWin32Window window)
        {
            _handle = window.Handle;
        }

        IntPtr IWin32Window.Handle
        {
            get { return _handle; }
        }
    }
}

Solution

  • Adding another answer because although it can be done this way, it's not the recommended way (e.g. should never have to call Application.DoEvents()).

    Use the pinvoke SetWindowLong to set the owner, however doing so then causes DoEvents to be required.

    A couple of your requirements don't make sense either. You say you want the dialog to minimize and maximize with the Excel window, but your code is locking up the UI thread, which prevents clicking on the Excel window. Also, you are using ShowDialog. So if the progress dialog was left open after finishing, the user still cannot minimize the Excel window because ShowDialog is used.

    public partial class MainForm : UserControl
    {
        public ManualResetEvent SignalEvent = new ManualResetEvent(false);
        private ProgressForm2 _progressForm;
        public volatile bool CancelTask;
    
        public MainForm()
        {
            InitializeComponent();
            this.Name = "MainForm";
            var button = new Button();
            button.Text = "Run";
            //button.Click += button1_Click;
            button.Dock = DockStyle.Fill;
            this.Controls.Add(button);
        }
    
        private void button1_Click(object sender, EventArgs e)
        {
            CancelTask = false;
            ShowProgressFormInNewThread();
        }
    
        internal void ShowProgressFormInNewThread()
        {
            var thread = new Thread(new ParameterizedThreadStart(ShowProgressForm));
            thread.Start(Globals.ThisAddIn.Application.Hwnd);
    
            //The main thread will block here until the signal event is set in the ProgressForm_Load.
            //this will allow us to do the work load in the main thread (required by VSTO projects that access the Excel object model),
            SignalEvent.WaitOne();
            SignalEvent.Reset();
    
            ExecuteTask();
        }
    
        private void ExecuteTask()
        {
            for (int i = 1; i <= 100 && !CancelTask; i++)
            {
                ReportProgress(i);
                Thread.Sleep(100);
    
                // as soon as the Excel window becomes the owner of the progress dialog
                // then DoEvents() is required for the progress bar to update
                Application.DoEvents();
            }
        }
    
        private void ReportProgress(int percent)
        {
            if (CancelTask)
                return;
            _progressForm.BeginInvoke(new Action(() => _progressForm.UpdateProgress(percent)));
        }
    
        private void ShowProgressForm(Object o)
        {
            _progressForm = new ProgressForm2(this);
            _progressForm.StartPosition = FormStartPosition.CenterParent;
    
            SetWindowLong(_progressForm.Handle, -8, (int) o); // <-- set owner
            _progressForm.ShowDialog();
        }
    
        [DllImport("user32.dll")]
        static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
    }