Thursday, September 25, 2008

A quick ASP.NET AJAX Thread Watcher

Thanks to this awesome article by Peter A. Bromberg i've unlocked the secrets of ASP.NET threading. Making it classy also makes it perpetually useful for dropping a chunk of business logic in any of your apps, not just the ASP ones.

So here is a VB.NET version of the above. Note that I have added in a parameter object to the class so you can pass that into the thread if need be. Also, the thread function makes a call to another function to do the actual work. So from this class that does the thread management, you can inherit another class that does the work, as well as add any other members you might want to update as the thread progresses.

Imports System.Threading

Public Class ThreadTask
Protected _firstRunComplete As Boolean = False


Public ReadOnly Property firstRunComplete() As Boolean

Get

Return _firstRunComplete

End Get

End Property


Protected _running As Boolean = False


Public ReadOnly Property Running() As Boolean

Get

Return _running

End Get

End Property


Protected _lastTaskSuccess As Boolean = True


Public ReadOnly Property LastTaskSuccess() As Boolean

Get

If (_lastFinishTime = Date.MinValue) Then

Throw New InvalidOperationException("The task has never completed.")

End If

Return _lastTaskSuccess

End Get

End Property


Protected _exceptionOccured As Exception = Nothing


Public ReadOnly Property ExceptionOccured() As Exception

Get

Return _exceptionOccured

End Get

End Property


Protected _lastStartTime As Date = Date.MinValue


Public ReadOnly Property LastStartTime() As Date

Get

If (_lastStartTime = Date.MinValue) Then

Throw New InvalidOperationException("The task has never started.")

End If

Return _lastStartTime

End Get

End Property


Protected _lastFinishTime As Date = Date.MinValue


Public ReadOnly Property LastFinishTime() As Date

Get

If (_lastFinishTime = Date.MinValue) Then

Throw New InvalidOperationException("The task has never completed.")

End If

Return _lastFinishTime

End Get

End Property


Protected _parameter As Object

Public ReadOnly Property Parameter() As Object

Get

Return _parameter

End Get

End Property


Public Sub RunTask(ByVal Parameter As Object)

SyncLock Me

If Not _running Then

Me._running = True

Me._lastStartTime = Date.Now

Dim t As Thread = New Thread(AddressOf Kickoff)

t.Start(Parameter)

Else

Throw New InvalidOperationException("The task is already running!")

End If

End SyncLock

End Sub


Protected Sub Kickoff(ByVal Parameter As Object)

Try

Me._parameter = Parameter

Dim ex As Exception = DoWork(Parameter)

If ex IsNot Nothing Then

Throw ex

End If

Me._lastTaskSuccess = True

Catch e As Exception

Me._lastTaskSuccess = False

Me._exceptionOccured = e

Finally

Me._running = False

Me._lastFinishTime = Date.Now

If Not Me._firstRunComplete Then Me._firstRunComplete = True

End Try

End Sub


Public Overridable Function DoWork(ByVal Parameter As Object) As Exception

Try

Thread.Sleep(8000)

Catch e As Exception

Return e

Finally

End Try

Return Nothing

End Function

End Class

Now derive something that actually does a useful job.

Public Class JobThread
Inherits ThreadTask

Public Overrides Function DoWork(ByVal Parameter As Object) As Exception
Try
Dim Job As Integer = Parameter
Dim dc As New JobDataContext
dc.CommandTimeout = 10000
dc.ProcessJob(Job)
Catch e As Exception
Return e
End Try
Return Nothing
End Function
End Class
Last but not least, if you are running a thread people will want to know what is happening with it, so based on the original page I've made a Web User Control you can drop on the page. Just call it's Start method when you need to and boom shanka, all done.

The control itself is designed to ensure only one thread is running. It also works between sessions as it runs the thread in the server cache under a unique global name. Useful if you close the wrong tab in your browser.
Markup:

<%@ Control Language="vb" AutoEventWireup="false" CodeBehind="JobWatcher.ascx.vb"
Inherits="JobWatcher" %>

<script language="JavaScript">
var sURL = unescape(window.location.pathname);

function refresh() {
window.location.href = sURL;
}
</script>

<asp:UpdatePanel ID="upnlWatch" runat="server">
<ContentTemplate>
<asp:Timer ID="tmrRefresh" runat="server" Interval="3000" Enabled="false">
</asp:Timer>
<asp:Panel ID="pnlWorking" runat="server" BackColor="AliceBlue" BorderStyle="Solid"
BorderWidth="1" Visible="false">
<asp:Label ID="msgLabel" runat="server"></asp:Label>
<asp:Image ID="imgTrobber" runat="server" ImageUrl="~/Images/Throbber-mac.gif" Visible="false" />
</asp:Panel>
</ContentTemplate>
<Triggers>
<asp:AsyncPostBackTrigger ControlID="tmrRefresh" EventName="Tick" />
</Triggers>
</asp:UpdatePanel>

Code Behind:

Public Class ComparisonThread
Inherits ThreadTask Imports System.ComponentModel
Partial Public Class JobWatcher
Inherits System.Web.UI.UserControl

Protected task As JobThread = Nothing
Protected _name As String = "JobWatcher"

Public Property Name() As String
Get
Return _name
End Get
Set(ByVal value As String)
_name = value
End Set
End Property

Public Sub Run(ByVal Parameter As Object)
If Cache(Me.Name) Is Nothing Then
task = New JobThread()
task.RunTask(Parameter)
Cache(Me.Name) = task
tmrRefresh.Enabled = True
pnlWorking.Visible = True
msgLabel.DataBind()
Else
msgLabel.Text = "There is already a Job running!"
End If
End Sub

Private Sub msgLabel_DataBinding(ByVal sender As Object, ByVal e As System.EventArgs) Handles msgLabel.DataBinding
If task IsNot Nothing Then
If task.Running Then
msgLabel.Text = _
"Job " & task.Parameter & " is running now.<br>" & _
"Time Started: " & task.LastStartTime.ToString() & "<br>" & _
"Time Elapsed: " & Math.Ceiling((Date.Now - task.LastStartTime).TotalSeconds) & " seconds."
Else
imgTrobber.Visible = False
tmrRefresh.Enabled = False
msgLabel.Text = "Job " & task.Parameter & " is <b>finished</b> now. <a href='javascript:refresh()'> (refresh page) </a><br>"
If task.LastTaskSuccess Then
msgLabel.Text &= "<font color='Green'>Job succeeded.</font>"
Else
msgLabel.Text &= "<font color='Red'>Job failed.</font>"
If task.ExceptionOccured IsNot Nothing Then
msgLabel.Text &= "<br>The exception was: " + task.ExceptionOccured.Message()
End If
End If
Cache.Remove(Me.Name)
End If
End If
End Sub

Private Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
task = CType(Cache(Me.Name), JobThread)
If task IsNot Nothing Then
If Not tmrRefresh.Enabled Then tmrRefresh.Enabled = True
If Not pnlWorking.Visible Then pnlWorking.Visible = True
msgLabel.DataBind()
Else
pnlWorking.Visible = False
End If
End Sub
End Class

No comments: