Tuesday, June 10, 2008

Updating the Main UI from a different thread

As you probably know, the availability of threading in .Net can lead to all kinds of little surprises, together of course with the power to write much more responsive applications.
For instance if you have a WinForms app that spawns a worker thread, and then that worker thread needs to update any UI elements on the main form, it will not work, since you can only update UI elements from the same thread they are created on.
I specifically ran into this when using a custom NUnit TestRunner (see previous post). It turns out, the TestRunner runs the test fixtures on a different thread, so you can't just do a frmMain.txtMessage.Text = "New Text from Test Fixture". Would have been too easy.

In addition, VB.Net also has the concept of default form instances, so when you do the above "frmMain.txtMessage" from a different thread than the main UI thread, VB.Net actually instantiates a new form, and then discards it when you are done. Thus there will be no exception raised for the above code, and putting in a brekpoint in the work thread shows the text properly updated. But in the UI the textbox will be empty.
It is much better explained in this post.

But the gist of it is this:

1. You have to use My.Application.OpenForms to get the correct UI thread instance of your form that you want to update

2. you have to use Invoke/BeginInvoke together with delegates to properly update the controls on the Main UI thread from a different thread.

3. The Control base class has a neat little method called InvokeRequired that will return True if the method is called from a different thread. This way you can write a generic function that works both on the UI thread and from other threads. Something like this:



Public Class frmMain

   'some magic voodo that needs to be done here to allow updating of the text box from a different thread
   Private Delegate Sub PostMessageDelegate(ByVal strNewMessage As String)

   'this is part 2 of the magic voodo: this is a public method that will be called on this form from the
   'outside, from different threads, so the text box can be updated
   Public Sub PostMessage(ByVal strMessage As String)
     If txtMessages.InvokeRequired Then
       'this method is called form a different thread, so we need to use BeginInvoke to update the UI
       txtMessages.BeginInvoke(New PostMessageDelegate(AddressOf PostMessage), New Object() {strMessage})
     Else
       txtMessages.Text &= Environment.NewLine & strMessage
     End If
   End Sub
...

End Class

Implement your own NUnit TestRunner - Part 2

After I blogged about creating a basic NUnit TestRunner in Part 1 of this series, this is the follow-up that shows how to extract and display the test results in a simple fashion.
Basically the TestResult object that is being returned from the Run method of the TestRunner contains probably most of the information you need.
Here is the Code:



Imports System.Text
Imports System.Xml
Imports System.IO
Imports System.Reflection
Imports System.Resources
Imports NUnit.Core
Imports NUnit.Util


Public Class Form1

   Private Sub btnRunTests_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnRunTests.Click

     Dim objTestRunner As TestRunner = Nothing

     objTestRunner = New RemoteTestRunner()

     Dim strMyFileName As String = Assembly.GetExecutingAssembly().Location
     Dim objPackage As TestPackage = New TestPackage(strMyFileName)


     objTestRunner.Load(objPackage)


     'run the test and create the result
     Dim objTestResult As TestResult = objTestRunner.Run(New NullListener)

     'get the result summary in XML format
     Dim strResultSummary As String = CreateXmlOutput(objTestResult)

     'now transform the XML into more display-friendly code using the default style sheet
     Dim sbResultSummary As New StringBuilder

     Dim objXFormReader As XmlTextReader = GetTransformReader(Nothing)
     Dim objXMLXform As XmlResultTransform = New XmlResultTransform(objXFormReader)
     objXMLXform.Transform(New StringReader(strResultSummary), New StringWriter(sbResultSummary))



     txtResults.Text = sbResultSummary.ToString()


   End Sub


   Private Shared Function CreateXmlOutput(ByVal objResult As TestResult) As String

     Dim sbXML As New StringBuilder()
     Dim objResultVisitor As New XmlResultVisitor(New StringWriter(sbXML), objResult)
     objResult.Accept(objResultVisitor)
     objResultVisitor.Write()

     Return sbXML.ToString()

   End Function 'CreateXmlOutput


   Private Shared Function GetTransformReader(ByVal strXSLFileName As String) As XmlTextReader

     Dim objXMLReader As XmlTextReader = Nothing
     If String.IsNullOrEmpty(strXSLFileName) Then
       Dim objNUnitAssembly As Assembly = Assembly.GetAssembly(GetType(XmlResultVisitor))
       Dim objResourceMgr As New ResourceManager("NUnit.Util.Transform", objNUnitAssembly)
       Dim strXmlData As String = CStr(objResourceMgr.GetObject("Summary.xslt"))

       objXMLReader = New XmlTextReader(New StringReader(strXmlData))
     Else
       Dim objXsltInfo As New FileInfo(strXSLFileName)
       If Not objXsltInfo.Exists Then
         Throw New FileNotFoundException("Transform file: {0} does not exist", objXsltInfo.FullName)
         objXMLReader = Nothing
       Else
         objXMLReader = New XmlTextReader(objXsltInfo.FullName)
       End If
     End If

     Return objXMLReader
   End Function 'GetTransformReader


End Class


this will then show the results in a user-friendly form in the text box txtResults.
Where this comes in handy is if you have a WinForms App and you want to run your Unit tests as part of the WinForms app without having to launch the NUnit GUI (or Console).
This is admittedly a very simple implementation, does not have a lot of the bells and whistles of a more graphical UI, but it tells you which tests failed and how long they took and where they failed.

Implement your own NUnit TestRunner - Part 1

I wanted to be able to run NUnit unit tests from my own test runner within a WinForms App. Easier said than done, not a lot of examples out there. I looked through the NUnit-Console code and the NUnit-Gui code and pieced together bits and pieces. Then I also stumbled across this blog post from a fellow german which helped with the last stumbling block:
http://achblah.blogspot.com/2007/04/monocov-and-my-contribution-to-it.html

The end result is this piece of VB.net code that will run any tests within your current EXE:

Imports System.Reflection
Imports NUnit.Core
Imports NUnit.Util


Public Class Form1

  Private Sub btnRunTests_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnRunTests.Click

    Dim objTestRunner As TestRunner = Nothing

    objTestRunner = New RemoteTestRunner()

    Dim strMyFileName As String = Assembly.GetExecutingAssembly().Location
    Dim objPackage As TestPackage = New TestPackage(strMyFileName)

    objTestRunner.Load(objPackage)

    objTestRunner.Run(New NullListener)



  End Sub
End Class



Of course this does not show any test results or exceptions because of using the "NullListener". In Part 2 I will fix that.