-1

I'm trying to use System.Net.Http.HttpClient to return the results of a call to a webpage, so that it implements a POST request. I don't really want to perform this asynchronously. My requirement is to wait until all the data is returned before continuing, so ideally I want synchronous method. However, sadly, it is not possible to just use HttpClient that way.

I've declared the following method, which is asynchronous, which takes a URL and key-value pairs to populate $_POST in the PHP:

private async Task<string> PostRequest(string cUrl, params string[] aParams)
{
    HttpClient oClient;
    Dictionary<string, string> oArgs;
    int iA;
    FormUrlEncodedContent oContent;
    HttpResponseMessage oResponse;

            
    // check we have an event number of parameters
    if ((aParams.GetUpperBound(0) % 2) != 1) throw new Exception(
        "Non-even number of parameters passed. Parameters are key-value pairs.");

    // put the parameters into a dictionary
    oArgs = new Dictionary<string, string>();
    for (iA = 0; iA < aParams.GetUpperBound(0); iA += 2)
        oArgs.Add(aParams[iA], aParams[iA + 1]);

    oClient = new HttpClient();
    oContent = new FormUrlEncodedContent(oArgs);
    oClient.Timeout = new TimeSpan(0, 0, 10);
    oResponse = await oClient.PostAsync(cUrl, oContent);
    return await oResponse.Content.ReadAsStringAsync();
}

Now, annoyingly this has to be an asynchonous method. Ho hum. Ideally, I'd like to call it thus:

private void button2_Click(object sender, EventArgs e)
{
    var cResult = await PostRequest("http://mywebsite.com/mypage.php",
        "MagicToken", "12345",
        "Method", "GetSomeData");
    txt.Text = cResult.ToString();
}

But I have the compile time error:

The 'await' operator can only be used within an async method. Consider marking this method with the 'async' modifier and changing its return type to 'Task'.

What I'm doing above is (obviously) a test. In reality the button that kicks this off is a "Next >" in a wizard. It will use the results to populate a structure with data that other code in the wizard then accesses. I don't the above to occur asynchronously as I don't want other code touching that structure until it is populated.

My question is, how can I wrap a call to PostRequest so that I can wait for all the results to come in (some sort of ...while still processing...wait... loop) and then just return the results of the call, and use that without having to bubble async declarations up through my code?

As a second question, if I have to declare my cmdNext_Click as async, what happens if the user clicks it twice? I specifically want the UI thread to stop until the data is returned and processed.


Edit:

I've tried creating a wrapper function (which is non-async) thus:

        private bool PostRequest2(string cUrl, ref string cResponse, params string[] aParams)
        {
            // This posts a request to the URL, using the parameters passed in aArgs. The response is returned in cResponse.
            // cUrl - the URL to POST to
            // cResponse - the response returned
            // aParams - an even number of parameters, which are key-value pairs. The first of each pair is the name of the item. The second is its value.
            int iWaitCount;

            try
            {
                var response = PostRequest(cUrl, aParams);
                Console.WriteLine(response);

                iWaitCount = 0;
                while (!response.IsCompleted)
                {
                    Console.WriteLine("iWaitCount = " + iWaitCount.ToString());
                    Console.WriteLine("Status = " + response.Status.ToString());
                    response.Wait(500);
                    iWaitCount++;
                }
                cResponse = response.Result;
                return true;
            }
            catch (Exception ex)
            {
                _g.Errs.Raise(ex);
                return false;
            }
        }

This compiles correctly, but sits in the wait loop indefinitely with response.Status = WaitingForActivation.

There has to be a way to wrap an asynchronouns function in a synchrnous one. The alternative is to have to change all the return types (which are mostly bool - true on success) to Task, which I cannot then use in conditional statements - I have to await them instead. I've realised that this is the fundimental question, and this is a duplicate of: How to call asynchronous method from synchronous method in C#? which refers to await being a zombie virus that infects your code; this appears to be the case.

Mark Roworth
  • 409
  • 2
  • 15
  • 1
    You can add the async modifier for your function, it will looks like this: `private async void button2_Click(...) { /* your code */ }` – Thomas Aug 10 '21 at 20:57
  • An asynchronous method is not guaranteed to run on a separate thread and thus asynchronously. Just use the `await` keyword and you will guarantee that the result will be populated before the next statement. – silkfire Aug 10 '21 at 22:15

4 Answers4

1

You can make your button void async (I would maybe return Task instead of void though)

await should mean that your method waits for the PostAsync call to complete. https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/await

The await operator suspends evaluation of the enclosing async method until the asynchronous operation represented by its operand completes.

So this is essentially a synchronous call.

Now if you really don't want that void to be async, here's what I can remember off the top of my head:

In .NET 5+, you can use HttpClient.Send which is synchronous. (takes HttpRequestMessage)

https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.send?view=net-5.0

Otherwise, you would need to do a .Result if you wanted to get the response. This type of consumption of async methods has been frowned upon in my experience.

  • thanks. I'm reassured by this. I'll probably just do this then. I still don't understand why, when await is used (so that the async task has completed), that it has to return a Task and not just a string? It feels like an awful breakage of encapsulation that all callers back to the UI have to change their interface to async and a task to return "something", instead of just returning the "something". Say I've got a call stack 10 calls deep. How do I actually unwrap the async and Task back to non-async and a string? – Mark Roworth Aug 11 '21 at 07:25
  • (sadly I'm using .Net 4.5. I'll consider upgrading) – Mark Roworth Aug 11 '21 at 07:26
0

Disable button2 until the operation is completed and use async inside the button2 click event.

change button2_Click to :

private async void button2_Click(object sender, EventArgs e)
{
    button2.Enabled = false;
    var cResult = await PostRequest("http://mywebsite.com/mypage.php",
      "MagicToken", "12345",
      "Method", "GetSomeData");
    txt.Text = cResult.ToString();
    button2.Enabled = true;
}
Meysam Asadi
  • 6,438
  • 3
  • 7
  • 17
0

After much reading, and thank you to those above, I've got a working method now. It's nor perfect, but it works in this scenario where I'm calling one async method at a time, and wait processing to stop until it returns.

PostRequest above works correctly, but it must be declared async and called with await. Within my app, I have a variety of callers of it, which must also be declared async and use await when they call it. An example is:

private async Task<bool> ReadProductPrice()
{
    string cCsv = "";

    try
    {
        cProductCode = scSubscriptionType.GetSelectedKey().ToString();
        var oResponse = await PostRequest("http://mywebsite.com/mywebpage.php",
            "MagicToken", "12345",
            "Query", "GetProductPrice",
            "ProductCode", cProductCode);
        if (oResponse == null) throw new Exception("Could not acquire product price from server. (1)");
        cCsv = oResponse.ToString();

        moProductPrice = new Dataset(_g);
        if (!moProductPrice.ReadFromCsv(cCsv)) throw new Exception("Could not decode server response.");
        if (moProductPrice.RecordCount != 1) throw new Exception("Could not acquire product price from server. (2)");

        return true;
    }
    catch (Exception ex)
    {
        _g.Errs.Raise(ex);
        return false;
    }
}

This works correctly and populates moProductPrice with the data returned from PostRequest. However, it is async.

I've create a wrapper function thus:

private bool ReadProductPrice2()
{
    Task<bool> oTask;
    frmWaitForTaskCompletion frm;

    try
    {
        oTask = ReadProductPrice();

        frm = new frmWaitForTaskCompletion();
        frm.WaitForTaskCompletion(oTask, "Waiting for product price from server...");
        return true;
    }
    catch (Exception ex)
    {
        _g.Errs.Raise(ex);
        return false;
    }
}

This passes the Task<bool> returned from ReadProductPrice through to a form. The form contains a Label and a Timer, named lblMessage and tmr, containing the following code:

    public partial class frmWaitForTaskCompletion : Form
    {
        private Task _task;

        public frmWaitForTaskCompletion()
        {
            InitializeComponent();
        }

        public void WaitForTaskCompletion<TResult>(Task<TResult> oTask, string cMessage)
        {
            _task = oTask;
            lblMessage.Text = cMessage;
            this.ShowDialog();
            return;
        }

        private void frmWaitForTaskCompletion_Load(object sender, EventArgs e)
        {
            tmr.Enabled = true;
        }

        private void tmr_Tick(object sender, EventArgs e)
        {
            if (_task.Status == TaskStatus.RanToCompletion) 
                this.Close();
        }
    }

The timer is set to an Interval of 1000 so that it shows for enough time for the user to recognise that a popup has occurred and to scan the message.

Ideally, I would like to replace the call to the wait form with this:

while (oTask.Status != TaskStatus.RanToCompletion) Thread.Sleep(100);

And I don't actually understand why this doesn't now work, but recognise that it doesn't; code never continues after this point, despite the fact that the wait form is effectively performing the same check.

In this way, I'm able to stop the await/async propogating up my call stack indefinitely; IMO should be the compiler's job to sort that out, not mine, and it signifcantly breaks the concept of encapsulation. I dislike the fact that I need to show a wait form for a short while, but in this context the user should be aware of the communication that is going on, so it's an ok solution.

Mark Roworth
  • 409
  • 2
  • 15
  • Please do knock holes in this if you can see an error/issue that will occur. I completely understand that this doesn't solve all scenarios. – Mark Roworth Aug 11 '21 at 10:26
-1

You can try doing something like this

private void button2_Click(object sender, EventArgs e)
{
  PostRequest(
    "http://mywebsite.com/mypage.php",
    "MagicToken",
    "12345",
    "Method",
    "GetSomeData"
  )
    .ContinueWith(async request => {
      var cResult = await request;
      txt.Text = cResult.ToString();
    })
    .Wait();
}