1

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; }
        }
    }
}
tjsmith
  • 729
  • 7
  • 21
  • Does it have to be a modal-dialog or can it be a non-modal dialog? You can also try creating a dummy class that implements `IWin32Window` and have it return the owner's `IntPtr` in the `Handle` property. – Loathing Mar 25 '15 at 22:45
  • Hi, thanks for your response. It has to be a modal-dialog. I have already tried a simple class that implements IWin32Window (as per this link: http://stackoverflow.com/questions/195635/how-do-i-pass-in-an-owner-window-to-messagebox-show-on-a-different-thread ) and also with a NativeWindow (which also implements IWin32Window) but both of them lock up the UI on the progress form. – tjsmith Mar 25 '15 at 23:15
  • You want to set the owner so that the progress form minimizes and restores with the main form. However, in your example the `ExecuteTask` is happening on the UI thread, which inhibits interacting with the main form until `ExecuteTask` is finished. – Loathing Mar 26 '15 at 09:12

2 Answers2

2

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);
}
Loathing
  • 5,109
  • 3
  • 24
  • 35
  • many thanks @Loathing! These changes did the trick. The minimizing I was referring to was via 'Show Desktop' functionality and not a minimize button(though I could also have a minimize button on my progress form that queues up an Excel minimize). The other important thing that having the owner for the modal form is that it locks the keyboard so that cells cannot be edited while the task is executing. If the modal form did not have an owner, the keyboard would not be locked within the active window and the user could inadvertently edit cells. – tjsmith Mar 30 '15 at 04:11
  • note that I was able to remove Application.DoEvents by putting this in ReportProgress(): _progressForm.Invoke(new Action(() => _progressForm.Refresh())); Do you foresee any issues with that change or is DoEvents() superior in any way? – tjsmith Mar 30 '15 at 08:36
  • That's neat. `Invoke` works, but not `BeginInvoke`. `Invoke` can end up deadlocking. You can google `C# invoke deadlock`. You can also read this post on why `DoEvents` is considered bad: http://stackoverflow.com/questions/5181777/use-of-application-doevents And pick which one seems better. It might not matter for a simple progress dialog. – Loathing Mar 30 '15 at 09:51
1

It's unusual to create winform controls on a non-UI thread. It's better to create the ProgressForm when the button is first clicked, then you don't need the ManualResetEvent.

Have the ProgressForm implement a simple interface (IThreadController) that allows your executing task to update the progress.

The owner of the ProgressForm is IntPtr handle = new IntPtr(Globals.ThisAddIn.Application.Hwnd);, which causes the ProgressForm to minimize and restore with the Excel window.

I don't think you need to use ShowDialog because it will block the UI thread. You can use Show instead.

E.g.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Threading;
using System.Runtime.InteropServices;

namespace ExcelAddIn1 {
public partial class UserControl1 : UserControl {

    public UserControl1() {
        InitializeComponent();
    }

    private void button1_Click(object sender, EventArgs e)
    {
        button1.Enabled = false;

        var pf = new ProgressForm();
        IntPtr handle = new IntPtr(Globals.ThisAddIn.Application.Hwnd);
        pf.Show(new SimpleWindow { Handle = handle });

        Thread t = new Thread(o => {
            ExecuteTask((IThreadController) o);
        });
        t.IsBackground = true;
        t.Start(pf);

        pf.FormClosed += delegate {
            button1.Enabled = true;
        };
    }

    private void ExecuteTask(IThreadController tc)
    {
        for (int i = 1; i <= 100 && !tc.IsStopRequested; i++)
        {
            Thread.Sleep(100);
            tc.SetProgress(i, 100);
        }
    }

    class SimpleWindow : IWin32Window {
        public IntPtr Handle { get; set; }
    }
}

interface IThreadController {
    bool IsStopRequested { get; set; }
    void SetProgress(int value, int max);
}

public partial class ProgressForm : Form, IThreadController {
    private ProgressBar _progressBar;
    private Label _progressLabel;

    public ProgressForm() {
        //InitializeComponent();
        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);
    }

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

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

    protected override void OnClosed(EventArgs e) {
        base.OnClosed(e);
        IsStopRequested = true;

    }

    public void SetProgress(int value, int max) {
        int percent = (int) Math.Round(100.0 * value / max);

        if (InvokeRequired) {
            BeginInvoke((Action) delegate {
                UpdateProgress(percent);
            });
        }
        else
            UpdateProgress(percent);
    }

    public bool IsStopRequested { get; set; }
}


}
Loathing
  • 5,109
  • 3
  • 24
  • 35
  • Hi thanks for your response. I've updated my post with some sample code trying to do show how to I am doing things as its a little hard to explain how I am doing things differently. The part of the code to which my question pertains in MainForm.cs in the ShowProgressForm function. In this example, passing the Window to ShowDialog will give a cross thread operation exception. – tjsmith Mar 26 '15 at 03:08
  • using the IThreadController interface definitely makes the code cleaner. The reason I chose to use ShowDialog instead of Show was because it stops the user from interacting with the Excel object model while the task is executing (cannot edit worksheet while the code is also editing it). I do turn set ExcelApp.ScreenUpdating = false which also stops user interaction. Perhaps I will be able to get away with using Form.Show() and ScreenUpdating = false. Will test some changes and report back. Thanks again for your help! – tjsmith Mar 26 '15 at 18:45
  • On your example code, you are running ExecuteTask on a secondary thread. I cannot do this as when interacting with the Excel object model, it must be done on the main thread. If I try to do a Form.Show() on a secondary thread, the form does not show because the thread gets disposed very quickly – tjsmith Mar 27 '15 at 04:35
  • What properties/methods is the `ExecuteTask` is using from Excel? Ideally, you would pull the data you need on the UI thread (which hopefully is quick), pass it to `ExcuteTask` for processing. Once `ExecuteTask` is done, do a `BeginInvoke` to update Excel with the result. – Loathing Mar 27 '15 at 06:04
  • The code will read potentially millions of rows of data from a csv or database and store those values in a multidimensional array and set Range.Value2 to the array. Since there could be massive amounts of data, the data is written in chunks. ExecuteTask may write 10 large chunks of data, create a new worksheet, write 10 more large chunks of data etc. since there is so much interaction with the Excel model, it cannot be postponed until the end because I would run out of memory. Writing each chunk of data to Excel is slow and may take a few seconds and will freeze any UI on the main thread. – tjsmith Mar 27 '15 at 07:25
  • One idea is to `BeginInvoke` after each chunk is processed. I'll try to see if it's possible to do what you are asking on the UI thread. – Loathing Mar 27 '15 at 07:48