Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create a WPF Progress Window on another thread

I'm creating a custom add-in command through the API of a piece of architectural modeling software called Revit. My command may take some time to complete so I want to show the user a window with a progress bar as it is working.

Typically if I were to create a progress window like this it would be on the main UI thread, and the actual work being done would happen on a secondary worker thread. However, Revit requires that any access to the API be through the thread calling the custom command. So I must create my progress bar on a second thread.

I found this blog post about launching a WPF window in a separate thread, and based my solution on it. Here's my custom command class.

public class SampleProgressWindowCommand : Autodesk.Revit.UI.IExternalCommand
{

    private ProgressWindow progWindow;
    internal static EventWaitHandle _progressWindowWaitHandle;


    public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
    {
        //Starts New Progress Window Thread
        using (_progressWindowWaitHandle = new AutoResetEvent(false))
        {

            //Starts the progress window thread
            Thread newprogWindowThread = new Thread(new ThreadStart(ShowProgWindow));
            newprogWindowThread.SetApartmentState(ApartmentState.STA);
            newprogWindowThread.IsBackground = true;
            newprogWindowThread.Start();

            //Wait for thread to notify that it has created the window
            _progressWindowWaitHandle.WaitOne();
        }

        //Does some work that takes a long time
        for (int i = 1; i <= 100; i++)
        {
            //Updates Progress
            this.progWindow.UpdateProgress("Item " + i.ToString(), i, 100);

            //Does some fake work
            System.Threading.Thread.Sleep(700);
        }

        //closes the Progress window
        progWindow.Dispatcher.Invoke(new Action(progWindow.Close));

        //Show Result to User
        Autodesk.Revit.UI.TaskDialog.Show("Task", "Task Completed");
        return Result.Succeeded;
    }

    private void ShowProgWindow()
    {
        //creates and shows the progress window
        progWindow = new ProgressWindow();
        progWindow.Show();

        //makes sure dispatcher is shut down when the window is closed
        progWindow.Closed +=new EventHandler(progWindow_Closed);

        //Notifies command thread the window has been created
        _progressWindowWaitHandle.Set();

        //Starts window dispatcher
        System.Windows.Threading.Dispatcher.Run();  
    }
}

And here is the UpdateProgress() method on my ProgressWindow class

public void UpdateProgress(string message, int current, int total)
{           
    this.Dispatcher.Invoke(new Action<string, int, int>(

    delegate(string m, int v, int t)
    {
        this.progressBar1.Maximum = System.Convert.ToDouble(t);
        this.progressBar1.Value = System.Convert.ToDouble(v);
        this.messageLbl.Content = m;
    }),
    System.Windows.Threading.DispatcherPriority.Background, 
    message, current, total);
}

My first question is in general did I do this right? It seems to work, but I know enough about multithreaded programing to know that just because it works today, does not mean it's going to work tomorrow.

Second, I would like to add a cancel button to my progress window to be able to cancel the process. What is the best way to do this? I understand that ultimately I'll end up with a "cancelRequested" boolean flag that is checked regularly by the working thread, but how do I set this from the progress window thread?

like image 581
Eric Anastas Avatar asked Aug 16 '13 18:08

Eric Anastas


1 Answers

The only improvement that I can see is that you have a potential race condition between setting your AutoResetEvent and calling Dispatcher.Run. I know because I've run into this issue in my own use of multi-threaded progress UIs.

The way to fix it is to BeginInvoke the call on the background Dispatcher. This will ensure it executes after the Dispatcher has begun pumping events:

System.Windows.Threading.Dispatcher.Current.BeginInvoke(
    new Func<bool>(_progressWindowWaitHandle.Set));
like image 68
Abe Heidebrecht Avatar answered Sep 27 '22 18:09

Abe Heidebrecht