I have a page on which I want to do some work when the user clicks an upload button. I want to disable the button while doing the work, and then re-enable it once the work is done. From what I’ve read, awaiting an async function should not block the UI. But when I click the button the UI is not showing the button change to disabled and it will also not show other actions, like clicking other buttons.
This is the exact code I encounter this problem with, I even replaced the call to the async function with a Task.Delay to make sure it wasn’t something weird with the function being called:
Protected Async Sub btnFileUpload_Click(sender As Object, e As EventArgs)
lblResponse.Text = String.Empty
btnFileUpload.Enabled = False
Await Task.Delay(5000)
btnFileUpload.Enabled = True
End Sub
The @Page is marked as Async="true", leaving this annotation out results in a runtime error.
Can anyone tell me why this would block the UI?
I have checked 10 other questions on stackoverflow like this one
C#/WPF Using async and await but the UI thread is still blocked
and this one
GUI freezes when using async/await , but the answer here always seems to be "Your async method is doing synchronous work.". But this shouldn’t be the case when it still happens with Task.Delay, I think.
2
Answers
Well, if you put a await in your code, then it going to wait, and defeat the whole purpose of async then, right?
Remember, you really can’t adopt async code for a web page, since the page and code must ALWAYS 100% complete first BEFORE the page then travels back to the client-side browser.
So, you have this:
Note how your page class – code behind is NOT in memory on the server.
YOU DO NOT have this:
And you do NOT even have this:
NOTE VERY careful here – the web page is ON CLIENT computer – it is NOT existing at all on the web server side.
So, when you click on a button, or do a post-back, then you get the start of the round trip.
This:
Our web page is sent up to the server. You now have this:
NOW an instance of the page class is created, and your code behind starts running.
Your code behind can modify controls (even controls to be visible or not), but the page is NOT interacting with the user – ONLY code can MODIFY the web page (user does not see one change, or many changes).
So, one change, or MANY changes to the web page can occur, but AS YOU update things like a text box etc., the user does NOT see these changes just yet. So, if you run a loop 1 to 10, and update a text box, the txt box WILL be updated 1 to 10. However, the end user sees nothing, since that web page (and code) is running up on the server.
Changes to web page are occurring on the server – the client-side browser does not have the web page any more!
Once ALL of your code is done running, then and ONLY then does the page make the trip back to the client-side browser.
The server-side class instance and code (and variables) are TOSSED OUT – DOES NOT exist! Your server-side page class is disposed – removed from memory, and the web page code behind does NOT exist any more.
So, page travels back to the client side, is re-displayed, JavaScript is loaded, and THEN JavaScript starts running. So even the client-side page now 100% re-loads.
So, now armed with the above knowledge of a post back and round type (the page life cycle)?
OK, this means that to display changes to the web page, the page MUST do a round trip.
So, if your code behind uses "await", then the page stays up on the server, and the end user just sees the browser spinner "wait" icon. You can await for 5 minutes, and the end user STILL does not see any changes, and the user is STILL waiting for the whole web page to travel back to the client side.
So, if you try to hide, or show controls with code behind? Then users will "eventually" see the changes, but only when 100% of the code has completed does the WHOLE web page make the trip back down to the client browser.
So, if you code behind "waits", then the web page is still stuck up on the server.
So, if you want to display some spinner, or wait while the page is up on the server processing?
Then you need to show that spinner BEFORE you post-back the page.
So, you can attach a client-side code that runs before the post-back occurs.
Hence this:
So, what the above does is show the div area with a spinner and message.
Note VERY close that when the page processing is done, we have no need to "re-hide" the message and spinner. Since a WHOLE NEW fresh page comes back from the server, the div area will re-hide on its own (revert back to the display none setting).
I assume jQuery for the above code. Give the above code a try.
My code behind for this test is:
So, when you click the button, the JavaScript runs, shows the spinner and wait animation gif. The page round type or so-called page life cycle starts. The page is posted up to the server, the page class (code behind) is created, and then your server code starts running.
When the code behind is 100% done, then the page makes the trip back to the client side. On the server side, that page class is now disposed, removed from memory, and the web server is now ready to process a web page from ANY user, not just you. This also means that a copy of the code on the server in memory no longer exists.
So, using await in a web page for server code does not make sense, since if you await, then the page has to wait up on the server, and thus you defeat the whole idea of using await.
So, keep in mind the above page life cycle. As long as your code behind is running, then the web page remains stuck up on the server until such time 100% of the code is complete, and then, and only then does the WHOLE web page make the trip back to the client side. Thus, changes made with code behind ONLY show to the end user when the page makes (completes) the trip back to the client side browser.
Thus, if you hide a button in code behind? Well, the user only going to see the button hide when the round trip and page life cycle is completed. So, that’s too late to hide the button. You thus have to hide the button, or show the message/animation before the page life cycle starts – you can’t achieve this "during" the page life cycle.
You can also of course adopt "ajax" calls, and in place of a page post-back, you have client side code call a web method. This will eliminate the post-back and round trip, but such calls require you to use "web methods". Such a call to the server will thus "wait" until the processing is completed. The downside of this approach is you need to write JavaScript code to make calls to that web method. Such calls are asynchronous from the JavaScript point of view, not the server point of view.
I recommend reading my article on
async
/await
on ASP.NET.First off, some clarification:
async
/await
doesn’t block GUI applications. But what you have is not a GUI application at all; it’s a web server. Web servers work by receiving HTTP requests and responding with HTTP responses. WebForms in particular kind of muddles this up and pretends that web servers are UI applications, so the HTTP requests and responses aren’t as obvious – and that’s unfortunate, since it leads to misunderstandings like the one that you had until recently.To quote my article:
Does this mean
async
on the server side is useless? Not at all; asynchronous requests allow your server to scale.That said,
async
is not an answer to your actual problem:What you need is a dynamic page. This is really only possible using JavaScript on the client side. There’s several different ways to tackle this, such as actually writing the JavaScript (what used to be called AJAX). WebForms has a kind of AJAX control called
UpdatePanel
that hides much of the JavaScript; you may be able to use that if you’re not ready to move to ASP.NET Core yet.