Search code examples
c#multithreadinguser-interfaceproducer-consumerrevit-api

Make main thread execute code on button press after form.show


I have a piece of code that does some calculations and then calls the form.show command. Now I have a library (the revit api) that does not allow me to store variables in a project without being in the main thread.

The logical solution for this is to get the spawned thread to call the main thread using say a producer/consumer pattern with code looking a bit like this:

form.Show(owner);
while(AppIsRunning){
    if(clicked)
        commit();
    else   
        Thread.sleep(100);
}

However when I do this the gui does not load fully (black background, no text in buttons ext.).

I have also tried doing this using the evoke method

    private void BtnOK_Click(object sender, System.EventArgs e)
    {
        Commit();
        Invoke(Commit);
    }

    private void Invoke(Action commit)
    {
        commit.Invoke();
    }

However this just tells me that it's not the main thread that's executing the commit function. Is there another way to do this or am I just making an error.

Just to be clear I have a form.show(owner) command that throws an error if it's not executed by the main thread. I also have a commit() function that must be excused by the main thread or it throws an error. The execution must wait until a button press. But the main thread polling the gui thread for changing causes the program to hang. According to my google search it' s also possible to do something involving an external event to get back into the right context but the example given was using python to invoke c# code, is there a good way to raise an external event to get back into a given thread in c#?

Edit: based on some suggestions I have created the following code:

public class ThreadManager
{
    static List<ThreadAble> orders = new List<ThreadAble>();
    public static bool running = false;
    public static void execute(ThreadAble action)
    {
        orders.Add(action);

    }

     static System.Timers.Timer timer;
    public static void RegisterAPIThreadAndHold(ExternalCommandData commandData)
    {

        UIApplication uiapp = commandData.Application;

        uiapp.Idling += Application_Idle;


    }


    private static void Application_Idle(Object o,IdlingEventArgs e)
    {
        if (orders.Count != 0)
        {
            ThreadAble f = orders.First();
            orders.Remove(f);
            f.execute();
        }
    }
}
public interface ThreadAble {
      void execute();        
}

However this does not appear to actually run when I use it as public override Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)

   Form frm = new OverviewForm(ExternalCommandData commandData);
   frm.show()
    ThreadManager.RegisterAPIThreadAndHold(commandData);
    ThreadManager.Execute(new run_ThrowError())

where ThrowError.execute() is Throw new Exception(" this is actually being executed" );


Solution

  • Your first example could work if you will replace Thread.Sleep by the System.Windows.Forms.Application.DoEvents(). It should give time to paint GUI and do not froze application completly.

    form.Show(owner);
    while(AppIsRunning){
        if(clicked)
            commit();
        else   
        {
            System.Windows.Forms.Application.DoEvents();
            // Thread.sleep(100);
        }
    }
    

    But this is not perfect solution to achieve this. Better would be calling Dispatcher.Invoke command inside your dialog to perform MainThread operations. You can use i.e. GalaSoft library - please refer to DispatcherHelper object documentation and samples.