Aleksey Malashenkov
Aleksey Malashenkov

Reputation: 125

Asynchronous function call of Windows Forms C#

I have an asynchronous function. It is called only once when the form is displayed for the first time. My function should ping devices asynchronously when I open the program. But it turns out that when you close the child form, another poll is launched. Tell me where the error may be.

Function call (I tried to call it in formLoad):

private async void MainForm_Shown(object sender, EventArgs e)
{
    await Start();
} 

Function itself:

public async Task Start()
{
    while (keyOprosDev)
    {
        for (int i = 0; i < devicesListActivity.Count; i++)
        {
            devicesListActivity[i].DevicesList.DevicesTotalPing++;

            string ipAdresDevice = devicesListActivity[i].DevicesList.DevicesName;
            int portDevice = devicesListActivity[i].DevicesList.DevicesPort;
            int activeDevice = devicesListActivity[i].DevicesList.DevicesActiv;
            int sendTimeDevice = devicesListActivity[i].DevicesList.DevicesTimeSend;
            int respTimeDevice = devicesListActivity[i].DevicesList.DevicesTimeResp;

            using (TcpClient client = new TcpClient())
            {
                if (activeDevice == 1)
                {
                    client.SendTimeout = sendTimeDevice;
                    client.ReceiveTimeout = respTimeDevice;

                    var ca = client.ConnectAsync(ipAdresDevice, portDevice);
                    await Task.WhenAny(ca, Task.Delay(sendTimeDevice));

                    client.Close();

                    if (ca.IsFaulted || !ca.IsCompleted)
                    {
                        textBox1.AppendText($"{DateTime.Now.ToString()} Server refused connection." + " " + ipAdresDevice + string.Format(" [{0}/{1}]", devicesListActivity[i].DevicesList.DevicesSuccessPing, devicesListActivity[i].DevicesList.DevicesTotalPing) + " " + System.Math.Round((double)(devicesListActivity[i].DevicesList.DevicesSuccessPing / devicesListActivity[i].DevicesList.DevicesTotalPing * 100)) + " %");
                        textBox1.AppendText("\r\n");
                        devicesListActivity[i].DevicesList.DevicesImage = 1;

                    }

                    else
                    {
                        devicesListActivity[i].DevicesList.DevicesSuccessPing++;
                        textBox1.AppendText($"{DateTime.Now.ToString()} Server available" + " " + ipAdresDevice + string.Format(" [{0}/{1}]", devicesListActivity[i].DevicesList.DevicesSuccessPing, devicesListActivity[i].DevicesList.DevicesTotalPing) + " " + System.Math.Round((double)(devicesListActivity[i].DevicesList.DevicesSuccessPing / devicesListActivity[i].DevicesList.DevicesTotalPing * 100)) + " %");
                        textBox1.AppendText("\r\n");
                        devicesListActivity[i].DevicesList.DevicesImage = 2;
                    }
                }
                else
                {

                }                                                   
            }
            await Task.Delay(interval);
        }                    
    }
}

And here is the opening of the child form:

try
    {
        DbViewer dbViewer = new DbViewer();
        dbViewer.FormClosed += new FormClosedEventHandler(refr_FormClosed);
        dbViewer.ShowDialog();
    }
    catch (Exception ex)
    {
        writeEventInDb(ex.Message);
    }

This is the event that handles the closure of the child form:

void refr_FormClosed(object sender, FormClosedEventArgs e)
{
    try
    {
        kryptonTreeView1.Nodes[0].Nodes[0].Nodes.Clear();
        kryptonTreeView1.Nodes[0].Nodes[1].Nodes.Clear();

        loadIpListFromDb();
        loadComListFromDb();

        kryptonTreeView1.ExpandAll();
    }
    catch (Exception ex)
    {
        writeEventInDb(ex.Message);
    }
}

Upvotes: 3

Views: 1663

Answers (1)

Rowan Smith
Rowan Smith

Reputation: 2180

You need to pass a cancellation token in. Somewhere outside of this code you need to create a CancellationTokenSource the best place is probably an property of the form:

class MainForm
{
    CancellationTokenSource cts;
    ...

You then initialize this and pass this to Start():

private async void MainForm_Shown(object sender, EventArgs e)
{
    cts = new CancellationTokenSource();
    CancellationToken ct = cts.Token;
    await Start(ct);
}

In your start loop you need to monitor for the cancellation token:

Because you're using a delay to timeout the ConnectAsync() you need the Task.Delay() to know when cancellation is requested so you need to pass the token to Task.Delay():

await Task.WhenAny(ca, Task.Delay(sendTimeDevice,ct));

After the TcpClient.Close() you need to test if the cancellation is requested, and stop loops if it is:

if (ct.IsCancellationRequested)
    break;

You'll need to perform the same test in the while loop, and also you should perform it immediately before the ConnectAsync(). While the most likely place you will encounter ct.IsCancellationRequested == true will be immediately after the Task.WhenyAny or immediately after the Loop interval there's no point starting a ConnectAsync() if cancellation has been requested.

You should also pass the CancellationToken to the Loop interval, otherwise you could end up waiting interval before your form closes:

// This will throw an OperationCancelled Exception if it is cancelled.
await Task.Delay(interval,ct);

Because you're going to continue anyway and just exit if Cancellation is registered you could avoid writing a separate try/catch that does nothing and await the interval like this, it's almost certainly less efficient, but it's cleaner.

// Leave any exceptions of Task.Delay() unobserved and continue
await Task.WhenAny(Task.Delay(interval,ct));

Finally you need to dispose of the CancellationTokenSource, I guess you would do this in something like a MainForm_Closed() function?

private void MainForm_Closed(object sender, EventArgs e)
{
    cts.Dispose();

The only thing left to do is to work out when you want to fire the CancellationRequest, based on what you have said you want to do this when the form close button has been clicked, so:

private void MainForm_Closing(object sender, EventArgs e)
{
    cts.Cancel();

That will cause the CancellationToken to transition to a cancelled state and your Start() routine will see that and exit.

In your code there is no single place to check for the CancellationToken being set, the rule of thumb is to check for it before and after any await and in your case you should check for it in both the while and the for loop.

Upvotes: 3

Related Questions