Search code examples
c#multithreadingautoresetevent

How to wait in main form for an event fired and completed in another thread without blocking the UI


I have a practical question about some code that I need to provide background information.

In my winform project I added a reference (Siemens.Sinumerik.Operate.Services.dll) to set, get and monitor some values of a NC (numerical control) grinding machine. With some specific values I can tell the machine to do some work (move grinding tools etc.). After the machine did the work, it gives me a feedback through another value. In my winform project I can monitor those events with so called hotlinks. So that means if the NC machine changes the value, an event is fired in my winform project.

My goal is that I start a job with setting some values and afterwards I wait for an answer of the machine.

So in my main Form I have a button click event that starts the job for the NC machine and it runs in my main thread. After I started the job the main thread needs to wait until the hotlink gives me a feedback and till the event is finished. The problem is that I have to define this hotlink after the documentation of Siemens. Every hotlink runs in a different thread and in its own class. Right now I blocked the main thread and the UI with the AutoResetEvent until the event in the different thread an class is fired and finished its work. The problem is that the UI is blocked, which I can't allow.

So my question is: how can I wait in the main thread for an event to be fired and finished in a different thread and class without blocking the UI?

Button click event in main form:

private static AutoResetEvent _waitHandle = new AutoResetEvent(false);

    // Button Click event that sets the values to cause NC machine to operate
    private void cmd_StartDauertest_Click(object sender, EventArgs e)
    {
        mySWE.SWETauschauftrag();

        _waitHandle.WaitOne();

        // more work will be done...
    }

class called "Schnellwechseleinheit.cs" that has the Event that is fired when the value of the NC machine changes. The class is devided in two parts. On half is executed in the class and the other one in the frm main but still in the thread of the class "Schnellwechseleinheit.cs".

First half of the class inside "Schnellwechseleinheit.cs":

class Schnellwechseleinheit
{
    public delegate void HotlinkSWEHasChanged();
    public event HotlinkSWEHasChanged HotlinkSWEChanged;

    DataSvc svc_initSWEHotlink = null;

    Guid guid_initSWEHotlink;


    /// <summary>
    /// Creates the "hotlink for the machine"
    /// </summary>
    public void initSWEHotlink()
    {
        DataSvc svc_SWEInit = null;
        svc_SWEInit = new DataSvc();
        Item SWEInit = new Item(MTU_Settings.Default.SWE_ERGEBNIS);
        SWEInit.Value = 0;
        svc_SWEInit.Write(SWEInit);

        svc_initSWEHotlink = new DataSvc();
        Item itemSubscribe = new Item(MTU_Settings.Default.SWE_ERGEBNIS);

        guid_initSWEHotlink = svc_initSWEHotlink.Subscribe(OnInitSWEHotlinkChanged, itemSubscribe);
    }


    /// <summary>
    /// This is the event of the Hotlink. Is caused when the value of the NC machine changes
    /// </summary>
    /// <param name="guid"></param>
    /// <param name="item"></param>
    /// <param name="Status"></param>
    private void OnInitSWEHotlinkChanged(Guid guid, Item item, DataSvcStatus Status)
    {
        try
        {

            DataSvc svc_SWEErg = null;
            svc_SWEErg = new DataSvc();
            Item SWEErg = new Item(MTU_Settings.Default.SWE_ERGEBNIS);
            svc_SWEErg.Read(SWEErg);

            if (Convert.ToInt16(SWEErg.Value) == 0)
            {
                writeStatSWE("Reset PLC Variable AMR Ergebnis für Auftragsstart!");
            }
            else if (Convert.ToInt16(SWEErg.Value) == 1)
            {
                writeStatSWE("Transportauftrag SWE wurde erfolgreich abgeschlossen!");
            }
            else
            {
                writeStatSWE("Transportauftrag SWE wurde von PLC abgelehnt :::: Fehlercode :::: " + SWEErg.Value.ToString());
            }

            this.HotlinkSWEChanged();
        }

        catch (Exception ex)
        {
            writeStatSWE(ex.Message);
        }
    }
}

Second half of the class "Schnellwechseleinheit.cs" in the form main.cs:

// Creating the object of the class "Schnellwechseleinheit" and adding the event
mySWE = new Schnellwechseleinheit();
        mySWE.initSWEHotlink();
        mySWE.HotlinkSWEChanged += mySWEHotlinkChanged;

/// <summary>
/// Second half of the hotlink (the event that is added)
/// </summary>
    private void mySWEHotlinkChanged()
    {

        if (mySWE.getSWEErg() == 1)
        {
            Werkzeug WZGetData = new Werkzeug();
            MagElements MagDataPocket1 = new MagElements();
            MagElements MagDataGreifer1 = new MagElements();
            MagElements MagDataGreifer2 = new MagElements();


            MagDataPocket1 = WZGetData.getWZData(21);
            if (MagDataPocket1 != null)
            {
                MagDataPocket1 = ToolsInMag[ToolsInMag.FindIndex(x => (x.TN == MagDataPocket1.TN && x.DN == MagDataPocket1.DN))];
                MagDataPocket1.ORT = myAMR.WriteORT(21);
                ToolsInMag[ToolsInMag.FindIndex(x => (x.TN == MagDataPocket1.TN && x.DN == MagDataPocket1.DN))] = MagDataPocket1;
            }

            MagDataGreifer1 = WZGetData.getWZData(10);
            if (MagDataGreifer1 != null)
            {
                MagDataGreifer1 = ToolsInMag[ToolsInMag.FindIndex(x => (x.TN == MagDataGreifer1.TN && x.DN == MagDataGreifer1.DN))];
                MagDataGreifer1.ORT = myAMR.WriteORT(10);
                ToolsInMag[ToolsInMag.FindIndex(x => (x.TN == MagDataGreifer1.TN && x.DN == MagDataGreifer1.DN))] = MagDataGreifer1;
            }

            MagDataGreifer2 = WZGetData.getWZData(11);
            if (MagDataGreifer2 != null)
            {
                MagDataGreifer2 = ToolsInMag[ToolsInMag.FindIndex(x => (x.TN == MagDataGreifer2.TN && x.DN == MagDataGreifer2.DN))];
                MagDataGreifer2.ORT = myAMR.WriteORT(11);
                ToolsInMag[ToolsInMag.FindIndex(x => (x.TN == MagDataGreifer2.TN && x.DN == MagDataGreifer2.DN))] = MagDataGreifer2;
            }

            _waitHandle.Set();

            UpdateMagDgv();

        }

Solution

  • One way to do this is to have a SemaphoreSlim property in the EventArgs that is signalled by the event handler when it has finished, and is awaited by the UI.

    For example, given a default Windows Forms application with a button "Button1", first define an EventArgs class like this:

    public sealed class MyEventArgs : EventArgs
    {
        public MyEventArgs(SemaphoreSlim finished)
        {
            Finished = finished;
        }
    
        public SemaphoreSlim Finished { get; }
    }
    

    The form defines an event handler like so:

    public event EventHandler<MyEventArgs> SomeEvent;
    

    The button click handler is:

    async void Button1_Click(object sender, EventArgs e)
    {
        var handler = SomeEvent;
    
        if (handler == null)
            return;
    
        Text = "Waiting for event to be handled.";
        button1.Enabled = false;
    
        using (var sem = new SemaphoreSlim(0, 1))
        {
            var args = new MyEventArgs(sem);
            handler(this, args);
            await sem.WaitAsync();
        }
    
        Text = "Finished waiting for event to be handled.";
        button1.Enabled = true;
    }
    

    Then the event can be subscribed to and handled like so (from the Program implementation):

    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
    
            using (var form = new Form1())
            {
                form.SomeEvent += onSomeEvent;
                Application.Run(form);
            }
        }
    
        static void onSomeEvent(object sender, MyEventArgs e)
        {
            Task.Run(() => handleEvent(e));
        }
    
        static void handleEvent(MyEventArgs e)
        {
            Thread.Sleep(4000);
            e.Finished.Release();
        }
    }
    

    Note how the handler starts a new task to handle the event, and signals the semaphore to indicate when it has finished.

    If you run this program and click the button, the title will change to "Waiting for event to be handled." for four seconds, and then change to "Finished waiting for event to be handled.",

    During this time, the UI is not blocked, because it is awaiting the semaphore.


    Alternatively, if the method handling the event is synchronous you can run it via a Task and await the task, without needing a semaphore.

    The event handler would be simply:

    public event EventHandler<EventArgs> SomeEvent;
    

    The button click handler would be:

    async void Button1_Click(object sender, EventArgs e)
    {
        var handler = SomeEvent;
    
        if (handler == null)
            return;
    
        Text = "Waiting for event to be handled.";
        button1.Enabled = false;
    
        await Task.Run(() => handler(this, EventArgs.Empty));
    
        Text = "Finished waiting for event to be handled.";
        button1.Enabled = true;
    }
    

    And the event handler itself can be implemented in class Program like so:

    using System;
    using System.Threading;
    using System.Windows.Forms;
    
    namespace WindowsFormsApp3
    {
        static class Program
        {
            [STAThread]
            static void Main()
            {
                Application.EnableVisualStyles();
                Application.SetCompatibleTextRenderingDefault(false);
    
                using (var form = new Form1())
                {
                    form.SomeEvent += onSomeEvent;
                    Application.Run(form);
                }
            }
    
            static void onSomeEvent(object sender, EventArgs e)
            {
                Thread.Sleep(4000);
            }
        }
    }