Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Async Await with Web Forms Over Multiple Postbacks

0.00/5 (No votes)
30 Dec 2015 1  
Better use async / await in web applications

Introduction

Support for asynchronous procedures (using async / await) is great in C# and VB languages, and works very well in desktop (WinForms, WPF, console and others) applications. But there are web applications, like based on ASP.NET Web Forms, where support for asynchronous procedures are much less exposed. Microsoft itself states, that support for asynchrony (using async / await or other methods) is only limited to offload working threads, increase throughput and all asynchronous procedures must complete work before rendering phase (generating HTML).

But there are also much more useful scenarios, where asynchronous procedure is started in some postback, and completed in one of subsequent postbacks, so executing spans over multiple postacks. This allows for UI driven asynchronous processing, which is useful for most web apps. There are no examples or explanations how to do this, and some folks even state that this is impossible. Fortunately, this is possible, and here is how to do it.

Description of Method

Everything necessary to do this is to feed await with non-started task, store this task and run it in some subsequent postback, on user's request. This way, any execution of continuations will be limited only to page processing phase and we have full control over it. But for our needs, using tasks is not the best option. It is better to create custom awaiters similar to triggers, and switch them on user's request, to continue execute of asynchronous procedures.

Guide how to create custom awaiters can be found here: link.

Code Examples

Here is the first example. This is code with html markup (for simplicity). Copy this example into a single file with name AsyncDemo1.aspx.

<!DOCTYPE html>

<script runat="server">
    Shared tr As TriggerAwaiter
    Shared fi As asyncdemo1_aspx

    Public Class TriggerAwaiter
        Implements Runtime.CompilerServices.INotifyCompletion

        Dim Action As Action = Nothing

        Public Sub RunAction()
            If Action Is Nothing Then Exit Sub
            Dim a = Action
            Action = Nothing
            a()
        End Sub

        Public ReadOnly Property IsCompleted As Boolean = False

        Public Sub OnCompleted(continuation As Action) _
        Implements System.Runtime.CompilerServices.INotifyCompletion.OnCompleted
            Action = continuation
        End Sub

        Public Sub GetResult()
        End Sub

        Public Function GetAwaiter() As TriggerAwaiter
            Return Me
        End Function

    End Class

    Protected Overrides Sub OnLoad(e As EventArgs)
        fi = Me
        MyBase.OnLoad(e)
    End Sub

    Protected Sub StartAsyncBTN_Click(sender As Object, e As EventArgs) Handles StartAsyncBTN.Click
        Dim af2 = Async Function()
                      Await GetTrigger()
                      Dim x = 0
                  End Function

        Dim af = Async Function()
                     fi.StartAsyncBTN.Enabled = False
                     fi.MakeProgresBTN.Enabled = True
                     fi.ResultTXT.Text = "async work started"
                     Await af2()
                     fi.ResultTXT.Text &= ", work 1"
                     Await af2()
                     fi.ResultTXT.Text &= ", work 2"
                     Await GetTrigger()
                     fi.ResultTXT.Text &= ", works finished"
                     fi.StartAsyncBTN.Enabled = True
                     fi.MakeProgresBTN.Enabled = False
                 End Function

        af()
    End Sub

    Public Function GetTrigger() As TriggerAwaiter
        tr = New TriggerAwaiter
        Return tr
    End Function

    Protected Sub MakeProgresBTN_Click(sender As Object, e As EventArgs) Handles MakeProgresBTN.Click
        tr.RunAction()
    End Sub

</script>

Also, modify web.config file by adding:

<add key="aspnet:UseTaskFriendlySynchronizationContext" value="false" />

into app settings region. This will ensure that LegacyAspNetSynchronizationContext is used as synchronization context. This context executes all continuations on a single thread.

For those who want to use newer AspNetSynchronizationContext, let's change web.config to:

<add key="aspnet:UseTaskFriendlySynchronizationContext" value="true" />

But additional work is necessary. AspNetSynchronizationContext executes different continuations using different threads, and we must track execution of continuations, to be able to wait when they finish their work or acquire new awaiter / trigger.

Here is a second example that shows how to wait for continuations executing on different threads. Copy this example into a single file with name AsyncDemo2.aspx.

<!DOCTYPE html>

<script runat="server">
    Shared tr As TriggerAwaiter
    Shared fi As asyncdemo2_aspx
    Shared AT As New AsyncTrack

    Public Class TriggerAwaiter
        Implements Runtime.CompilerServices.INotifyCompletion

        Dim Action As Action = Nothing

        Public Sub RunAction()
            If Action Is Nothing Then Exit Sub
            Dim a = Action
            Action = Nothing
            a()
        End Sub

        Public ReadOnly Property IsCompleted As Boolean = False

        Public Sub OnCompleted(continuation As Action) _
        Implements System.Runtime.CompilerServices.INotifyCompletion.OnCompleted
            Action = continuation
        End Sub

        Public Sub GetResult()
        End Sub

        Public Function GetAwaiter() As TriggerAwaiter
            Return Me
        End Function

    End Class

    Public Class AsyncTrack
        Private Class WaitingTasks
            Public Awaiter As TriggerAwaiter = Nothing
            Public Tasks As New List(Of Threading.Tasks.Task)
        End Class

        Dim SO As New Object
        Dim WTS As New List(Of WaitingTasks)
        Dim ExecWT As WaitingTasks = Nothing

        Public Sub SetAwaiter(Awaiter As TriggerAwaiter)
            SyncLock SO
                If ExecWT Is Nothing Then
                    ' awaiter created by tracked continuations
                    ' applying it to currently tracked continuations
                    WTS.Add(New WaitingTasks With {.Awaiter = Awaiter})
                Else
                    ' awaiter created before continuations
                    ' creating new context for continuations that follow after this awaiter
                    ExecWT.Awaiter = Awaiter
                End If
            End SyncLock
        End Sub

        Public Function TrackSub(SubProc As Func(Of Threading.Tasks.Task)) As Threading.Tasks.Task
            Dim t = SubProc()
            If t.Status = Threading.Tasks.TaskStatus.RanToCompletion Then Return t
            ' task will be completed in future, registering it for tracking while waiting in future
            SyncLock SO
                WTS.Last.Tasks.Add(t)
            End SyncLock
            Return t
        End Function

        Public Function TrackProc(Of T1)(SubProc As Func(Of Threading.Tasks.Task(Of T1))) As Threading.Tasks.Task(Of T1)
            Dim t = SubProc()
            If t.Status = Threading.Tasks.TaskStatus.RanToCompletion Then Return t
            ' task will be completed in future, registering it for tracking while waiting in future
            SyncLock SO
                WTS.Last.Tasks.Add(t)
            End SyncLock
            Return t
        End Function

        Public Sub SwitchAndWait(Awaiter As TriggerAwaiter)
            SyncLock SO
                ' searching context for awaiter
                ExecWT = WTS.Where(Function(x) x.Awaiter Is Awaiter).FirstOrDefault()
                If ExecWT Is Nothing Then Exit Sub
                ExecWT.Awaiter = Nothing
            End SyncLock

            ' calling continuations
            Awaiter.RunAction()

            Dim c = 0
            While 1
                SyncLock SO
                    ' checking if new awaiter was registered by continuations
                    If ExecWT.Awaiter IsNot Nothing Then
                        ExecWT = Nothing
                        Exit Sub
                    End If

                    ' checking if all continuations was executed to the end
                    If ExecWT.Tasks.All(Function(x) x.Status = _
				Threading.Tasks.TaskStatus.RanToCompletion) Then
                        ' all continuations executed to the end - exit waiting
                        Exit While
                    Else
                        ' some continuations still executing on other threads
                        ' give them some time to complete
                        Threading.Thread.Sleep(10)
                        c += 1
                    End If
                End SyncLock
            End While

            ' cleaning
            SyncLock SO
                WTS.Remove(ExecWT)
                ExecWT = Nothing
            End SyncLock
        End Sub

    End Class

    Protected Overrides Sub OnLoad(e As EventArgs)
        fi = Me
        MyBase.OnLoad(e)
    End Sub

    Protected Sub StartAsyncBTN_Click(sender As Object, e As EventArgs) Handles StartAsyncBTN.Click
        Dim af2 = Async Function()
                      Await GetTrigger()
                      fi.ResultTXT.Text &= ", pre work _
                      (TH:" & Threading.Thread.CurrentThread.ManagedThreadId & ")"
                      Dim n = 0
                  End Function

        Dim af = Async Function()
                     fi.StartAsyncBTN.Enabled = False
                     fi.MakeProgresBTN.Enabled = True
                     fi.ResultTXT.Text = "async work started"
                     Await af2()
                     fi.ResultTXT.Text &= ", work 1 _
                     (TH:" & Threading.Thread.CurrentThread.ManagedThreadId & ")"
                     Await af2()
                     fi.ResultTXT.Text &= ", work 2 _
                     (TH:" & Threading.Thread.CurrentThread.ManagedThreadId & ")"
                     Await GetTrigger()
                     fi.ResultTXT.Text &= ", works finished _
                     (TH:" & Threading.Thread.CurrentThread.ManagedThreadId & ")"
                     fi.StartAsyncBTN.Enabled = True
                     fi.MakeProgresBTN.Enabled = False
                 End Function

        AT.TrackSub(af)
    End Sub

    Public Function GetTrigger() As TriggerAwaiter
        tr = New TriggerAwaiter
        AT.SetAwaiter(tr)
        Return tr
    End Function

    Protected Sub MakeProgresBTN_Click(sender As Object, e As EventArgs) Handles MakeProgresBTN.Click
        AT.SwitchAndWait(tr)
    End Sub

</script>

You do not have to track every procedure, only 'outer' asynchronous procedures, where asynchronous processing starts.

Points of Interest

I invented this method mainly for my needs. Just wanted to remove the previous solution based on traditional approach (without using async / await), because it was convoluted and problematic to use.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here